# Toolkit Administration Guide

Welcome to the administration guide for UDiTH App Toolkit. This guide provides detailed instructions for managing the application efficiently.

# License

The **License Management** section allows administrators to manage the application's license key. This ensures compliance and enables access to application features. All license keys are granted by CAXperts.

## Features

### 1. **License Key Input**

- Provides a secure interface to input or update the license key.

- Dynamically toggles between masked and unmasked views for the license key by using the **Eye** icon to the right of the textbox.

![License input masking](media/admin_guide_license_management.gif)

### 2. **Validation and Feedback**

- Displays toast notifications to confirm successful updates.

- Ensures that the provided license key is valid and up-to-date.

- If the license key is invalid, an error notification is displayed, and the following screen is shown.

![Invalid License](media/admin_guide_invalid_license.png)

## License Management Actions

### **View and Edit License Key**

1. Navigate to the **License** page.

2. The current license key is preloaded in the input field upon page load.

3. To update the license key:

- Enter the new key in the text field.

- Use the **Eye** icon to toggle between masked and unmasked views of the key.

### **Submit the License Key**

1. After entering the new key, click the **Upload** button.

2. The system performs the following actions:

- Validates the license key.

- Updates the license key for the project.

- Displays a success notification confirming the update.

3. Upon successful update, you are redirected to the **Home** page.

# Roles

The **Roles** page serves to control which roles are available in the application. 
These roles are sourced from Keycloak and are used to populate dropdown selections within the system. This table does not configure user permissions but user responsibilities e.g. used in work flows.

![The roles page](media/admin_guide_roles.png)

### Tip

- Double-check role edits before pressing Enter to avoid accidental changes.



## Keycloak User Synchronization

The Toolkit includes an automated synchronization system that keeps user accounts and their assigned roles in sync between **Keycloak** (the identity provider) and the **application database**.

### How the Synchronization Works

- **Keycloak is the Source of Truth**: The synchronization process treats Keycloak as the authoritative source for user roles and group assignments.

- **Automated Daily Sync**: By default, the system runs a scheduled synchronization job **every day at 3:00 AM** (03:00).

- **What Gets Synchronized**:
  - All users with email addresses in Keycloak are processed
  - User group memberships in Keycloak are compared with role assignments in the database
  - Missing roles are **added** to the database
  - Extra roles (present in database but not in Keycloak) are **removed**
  - User accounts are created or updated as needed

### Configuration

The synchronization behavior can be customized using environment variables in your deployment configuration:

#### Enable/Disable Synchronization

```
ENABLE_CRON=true
```

Set this to `true` to enable the cron job system (including user synchronization). Set to `false` or leave empty to disable.

#### Customize Sync Schedule

```
KC_GROUP_SYNC_CRON=0 3 * * *
```

This variable controls when the user synchronization runs using standard cron expression syntax (5-field format):

- **Default**: `0 3 * * *` (runs daily at 3:00 AM)
- **Format**: `minute hour day-of-month month day-of-week`
- **Examples**:
  - `0 2 * * *` — Run daily at 2:00 AM
  - `0 */6 * * *` — Run every 6 hours
  - `0 0 * * 0` — Run weekly on Sunday at midnight
  - `30 1 * * 1-5` — Run weekdays at 1:30 AM

**Note**: The cron expression must be valid. If an invalid expression is provided, the system will default back to `0 3 * * *` and log a warning.

# Workflows

The **Workflows** page allows administrators to create, view, edit, delete and assign default workflows.

![The workflows administration page](media/admin_guide_workflows_page.png)

## Overview

- Workflows are assigned to specific punch items. They are used to define the steps that must be completed to resolve a punch.

- The admin defines those steps and who is authorized to complete them.

- When the workflow is successfully finished (once the last step is approved), and if the last step contains a coded step closeWorkflow(), the punchlist item is closed.

### 1. Workflow Navigation

- The Workflow Management page displays a list of existing workflows and their details.

- Admins can add new workflows or edit existing ones.

![Navigating the workflow](media/admin_guide_navigating_workflow.gif)

### 2. Default Workflow Assignment

- The **Assign Default Workflow** feature lets administrators designate or reassign the workflow that is automatically applied to new punch items.

- This option is accessible by clicking on the Workflow Tools dropdown menu in the management pane.

![Using the assign default workflow feature](media/admin_guide_assign_default.gif)

### 3. Workflow Interface

- Administrators can create a new workflow by clicking the **Add Workflow** button.

- A popup prompts users to enter a unique workflow title.

![Adding a new workflow](media/admin_guide_add_workflow.gif)

- Once inside a new/existing workflow, the administrator can start adding steps using the **Plus** button in the top right of the page.

- Once a step is added the different fields in the row can be edited by double clicking in the respective cell.

- Step Number, Title, Description and Responsible Person are all mandatory fields and must be filled in otherwise an error will be thrown.

- After making any changes, the **Save** button in the top right will become active and display a red background. Administrators can save their modifications by clicking this button.

- The step number denotes the order in which the steps will need to be completed to successfully finish the workflow.

- The workflow step can have different use actions: "None", "Activity", "Decision".

  "None" does not require any interaction from a user.

  "Activity" requires the user to approve or disapprove the step with a button click.

  "Decision" requires the user to approve or disapprove with or without signature.

### 4. Workflow Columns

- **Steps Considered for Progress**: Defines which workflow steps are included when calculating progress after reaching a specific step. When a step (e.g., Step 5) is reached, the system compares the steps listed here with the steps completed so far. For example, if Step 5 can only be reached after Steps 1–4 and is the final step, enter `1,2,3,4,5`. This allows customizing progress for each workflow step, enabling non‑linear workflows. The field accepts comma‑separated numbers representing workflow steps.

- **Responsible Person** column specifies who is authorized to approve or deny the workflow step. The roles that are shown are roles that are specified by admin in Keycloak Groups.

- **Forward Step ID** column specifies what is the next workflow step when the pending step is approved.

- **Backward Step ID** column specifies what is the next workflow step when the pending step is disapproved.

> **Note:** The default behavior of the **Forward Step ID** and **Backward Step ID** columns can be overridden using [Custom Code](#custom-code) functions:
>
> - `stepForwardJumpTo(stepNumber: number, message: string | null)`: Jumps to the specified step number and, if a message is provided, it is printed to the activity chat. This jump is treated as a success.
> - `stepBackwardJumpTo(stepNumber: number, message: string | null)`: Jumps to the specified step number and, if a message is provided, it is printed to the activity chat. This jump is treated as a failure.
>
> When used, these functions take **priority over the Forward Step ID and Backward Step ID values** defined in the workflow table.

### 5. Workflow Code

Allows users to define [Custom Code](#custom-code) that is run when workflow step is accepted or disapproved.

Before saving the code, it must be compiled with a mockup interface that does not influence original punch items. The admin can choose different punches for examplary compilation if they specify a number in "PunchlistID for compilation" field.
If the compilation is successful the code can be saved.

> **Related Features:**
>
> - Use [Event Listeners](#event-listeners) to trigger actions based on workflow events (step acceptance, field changes, etc.)
> - Use [Field Validators](#field-validators) to enforce data validation rules before field values are saved

### 6. Workflow Email Message

Allows users to send an email with the provided html body to the specified responsible group in the workflow step.

**Available Dynamic Variables:**

- **Ticket Fields** - Include any punch field value:

  ```
  ${Details["<field name>"]}
  ```

  Example: `Pipe in Area System: ${Details["Area System"]} broken.`

- **Ticket ID** - Include the punch item ID:

  ```
  ${PunchItem}
  ```

  Example: `Ticket #${PunchItem} requires attention.`

- **Server Address** - Include the application server URL:

  ```
  ${serverAddress}
  ```

  Example: `View ticket at: ${serverAddress}/tickets/${PunchItem}`

- **Current Date** - Include formatted date using the Date() function:

  ```
  ${Date("format")}
  ```

  **Common Date Formats:**
  - `${Date("yyyy-MM-dd")}` → 2025-12-01
  - `${Date("MM/dd/yyyy")}` → 12/01/2025
  - `${Date("dd-MM-yyyy")}` → 01-12-2025
  - `${Date("yyyy-MM-dd HH:mm:ss")}` → 2025-12-01 14:30:00
  - `${Date("MMMM dd, yyyy")}` → December 01, 2025

  Example: `This notification was sent on ${Date("yyyy-MM-dd")}`

**Email Template Example:**

```html
<h2>Ticket Update Notification</h2>
<p>Dear Team,</p>
<p>Ticket <strong>#${PunchItem}</strong> has been updated.</p>
<ul>
  <li><strong>Status:</strong> ${Details["Status"]}</li>
  <li><strong>Priority:</strong> ${Details["Priority"]}</li>
  <li><strong>Area System:</strong> ${Details["Area System"]}</li>
  <li><strong>Date:</strong> ${Date("MMMM dd, yyyy")}</li>
</ul>
<p>
  <a href="${serverAddress}/tickets/${PunchItem}"
    >Click here to view the ticket</a
  >
</p>
<p>This is an automated notification from ${serverAddress}</p>
```

**Note:** Subject lines can also use dynamic variable replacement with the same syntax shown above.

# Workflows Kanban

The **Kanban Settings** page allows administrators to configure the appearance of the Kanban cards that appear in the "**Planning**" section by selecting fields for the title, subtitle, and content zones. These settings impact how information is displayed within the Kanban view, enabling a tailored experience to suit specific workflows.

![The workflows kanban settings page](media/admin_guide_kanban.png)

## Kanban Settings Actions

- **Purpose**: Define what information appears on the Kanban cards for the title, subtitle, and content areas.

- **How to Use**:
  - Open the drop-down menus for **Title**, **Subtitle**, and **Content**.

  - Select a field from the list of available options, which are fetched from the system’s punch details available fields.

![Adjusting the fields](media/admin_guide_kanban_field_adjust.gif)

- Each selection updates the Kanban card preview in real time, allowing you to instantly see how your choices affect the card layout. The preview is displayed to the right of the select boxes.

- The values from the fields you select in the drop-down menus will automatically populate the corresponding sections of each individual Kanban card.

![Utilizing the drop-down menus](media/admin_guide_kanban_dropdown_menu.gif)

- After finalizing your selections, click the **Save** button.

- A confirmation message will appear, notifying you that the settings have been saved successfully.

# Punchlist Fields

The **Punchlist Fields** page allows administrators to add, configure and manage available punchlist fields effectively. These fields define how information is structured and displayed in punchlists, ensuring adaptability to various workflows and requirements.

![Punchlist fields](media/admin_guide_punchlist_fields.png)

## Managing Punchlist Fields

Administrators can add, edit, delete, and reorder punchlist fields using the following actions:

### Adding a New Punchlist Field

1.  **Add a Field:**

- Click the **Plus (+) button** in the top right corner to add a new punchlist field.

2.  **Fill in Field Details:**

Fields are accessed by single-clicking on the respective cell.

- **Detail (Required):** Enter the name of the punchlist field.

- **Type (Required):** Select the field type. See options below.
  - **dxTextBox:** A textbox for user input of text data.
  - **dxSelectBox:** A dropdown select box allowing users to choose from predefined options. These options are specified by the administrator in the "**Items**" column.
  - **dxDateBox:** A date picker for selecting dates.
  - **dxHyperLink:** A hyperlink element that navigates to a user specified URL.
  - **Breakline:** Inserts a horizontal line to separate content sections in punchlist user form.

- **Additional Options:**

- **Column Span:** Specify the number of columns this field should occupy on the punch item's detail page.

- **Default View:** Choose whether this field should be displayed by default in the main punchlist table.

- **Items (for Selectbox):** If the type is Selectbox, specify the available options in the **Items** column.

- **Editors:** Grant the selected roles permission to edit the specified field. The field will be in a read-only state for roles not included in this option.

![Making a field read-only](media/admin_guide_editors_fields.gif)

3.  **Save or Revert:**

- **Save:** Click the **Save** button at the end of the row to save the new field.

- **Revert:** Click the **Revert** button to discard the new entry.

### Editing an Existing Punchlist Field

- Click the **Pencil icon** at the end of the row corresponding to the field you wish to edit.

- Modify the necessary details.

- Click **Save** to apply changes or **Revert** to cancel.

### Deleting a Punchlist Field

- Click the **Trash icon** at the end of the row for the field you want to delete.

- **Warning:** Deleted fields cannot be recovered. Confirm the deletion when prompted.

### Reordering Punchlist Fields

- **Drag and Drop:**

- Click and hold the **Six-Dot icon** (⋮⋮) to the left of a row.

- Drag the row to your desired position within the table.

- Release to drop the row in the new position.
- The order of the rows in the table defines the order in which the fields appear in the punchlist user form.

![Re-ordering fields](media/admin_guide_punchlist_reordering_items.gif)

# Attribute Mapping

The **Attribute Mapping** section enables administrators to define relationships between ticket detail fields and model attributes. These mappings are applied to the comments of ticket items that are created using the "**Create with Comment**" option in main tickets table.

![The attributes mapping page](media/admin_guide_attribute_mapping.png)

## Features

### 1. **Mapping Interface Overview**

- **Punch Details List**: Displays un-mapped ticket details available for mapping.

- **Mapped Attributes List**: Lists existing mappings with details such as the mapped attribute(s) and type (2D, 3D, or Code).

### 2. **Mapping Actions**

**Add Detail to Mapping**

1. Click the "+" button to move the detail to the mapped attributes list.

2. The detail is now ready to be mapped to an attribute.

3. Select up to 3 ticket details to generate a custom ticket comment.

![Adding detail to mapping](media/admin_guide_mapping_adding_detail.gif)

**Map Attribute to Detail**

- In the **Mappings** pane click the **Edit** button of the field you would like to map an attribute to.

- This will open a pop-up that has options to either map to a 3D, 2D or custom code attribute.

- Click on any of these options to choose a mapping type:
  - **3D Single Mapping**: Map the field to a 3D attribute. A list is available for the administrator to select from which contains all possible model attributes.

  - **2D Single Mapping**: Map the field to a 2D attribute. These attributes can be user-defined in the textbox.

  - **Code Mapping**: Map the field using a [Custom Code](#custom-code)

- Save changes to finalize the mapping.

![Editing map attribute](media/admin_guide_mapping_editing_attribute.gif)

Creating a ticket with the option "Create with Comment" now reflect changes made with regards to attribute mapping:

![Create with Comment](media/admin_guide_createwithcomment.gif)

**Delete Mapping**

- Click the **Trash** button next to a mapped attribute to remove the mapping.

- The detail returns to the **Ticket Details** list for future use.

# Event Listeners

The **Event Listeners** section provides administrators with tools to manage event-driven automation throughout the Toolkit application. Event listeners enable custom actions or behaviors triggered by specific conditions, such as field value changes, workflow actions, ticket creation, email sending, or scheduled intervals.

Unlike [Field Validators](#field-validators) which **prevent** invalid data from being saved, event listeners **react** to events after they occur, executing custom code to automate workflows, send notifications, update related fields, or trigger external integrations.

![The event listener section](media/admin_guide_event_listeners.png)

## Understanding Listener Types

Each listener must have a **Type** that determines when and how it triggers. The listener's required fields vary based on the selected type. Administrators must select the appropriate type and configure type-specific fields to create functional listeners.

### Available Listener Types

#### 1. onWorkflowAccepted

**Description:** Triggers when a specific workflow step is accepted by a user.

**When it triggers:** After a user approves/accepts a workflow step and the step status changes to "Accepted".

**Required Fields:**

- **Type**: `onWorkflowAccepted`
- **Checked Field**: Step number (e.g., "1", "2", "3")
- **Checked Value**: "Accepted"
- **Workflow ID**: The workflow this listener applies to
- **Custom Code**: JavaScript code to execute when triggered

**Use Cases:**

- Send notification when critical step is approved
- Auto-populate fields after approval
- Create comments or logs for audit trail

**Example:**

```
Type: onWorkflowAccepted
Checked Field: 3
Checked Value: Accepted
Workflow ID: 5
Custom Code: sendMailTo(["manager@example.com"], "Step 3 approved", "Workflow Progress")
```

#### 2. onPunchlistDetail

**Description:** Triggers when a specific punch detail field value changes to a specified value.

**When it triggers:** After a ticket field is updated and the new value matches the specified CheckedValue.

**Required Fields:**

- **Type**: `onPunchlistDetail`
- **Checked Field**: Field name (e.g., "Status", "Priority")
- **Checked Value**: The value that triggers the listener (e.g., "Closed", "High")
- **Workflow ID** (or **Default**: true): Either specify a workflow or make it workflow-independent
- **Custom Code**: JavaScript code to execute when triggered

**Use Cases:**

- Notify team when status changes to "Closed"
- Auto-populate related fields when priority is set
- Trigger external API when specific conditions are met

**Example:**

```
Type: onPunchlistDetail
Checked Field: Status
Checked Value: Closed
Default: false
Workflow ID: 5
Custom Code: setTicketValue("ClosedDate", Date())
```

#### 3. onTicketCreated

**Description:** Triggers when a new ticket is created.

**When it triggers:** Immediately after a new ticket is created in the system.

**Required Fields:**

- **Type**: `onTicketCreated`
- **Default**: Typically set to `true` (apply to all new tickets)
- **Custom Code**: JavaScript code to execute when triggered (can include filtering logic)

**Use Cases:**

- Auto-assign new tickets based on criteria
- Set initial field values
- Send creation notifications
- Apply default workflows

**Example:**

```
Type: onTicketCreated
Default: true
Custom Code:
  const area = getTicketValue("Area");
  if (area === "Engine Room") {
    setTicketValue("AssignedTo", "engineering@example.com");
  }
```

#### 4. onEmailSend

**Description:** Triggers when an interaction email is sent from the support ticket system, filtered by regex patterns matching the email subject and/or body content.

**When it triggers:** After an email is sent through the Interactions feature, if the email subject or body matches the specified regex patterns.

**Required Fields:**

- **Type**: `onEmailSend`
- **Subject Regex**: Regular expression pattern to match email subject (max 500 characters)
- **Content Regex**: Regular expression pattern to match email body (max 500 characters)
- **Custom Code**: JavaScript code to execute when triggered

**Important Notes:**

- The system tracks processed emails using the `ProcessedEmailListeners` table to prevent duplicate triggers for the same email
- Both regex fields are optional, but at least one should be specified for effective filtering
- Regex patterns are case-sensitive unless specified otherwise in the pattern

**Use Cases:**

- Track specific keywords in outgoing emails
- Log emails containing certain phrases
- Trigger actions based on email content (e.g., escalate tickets with "urgent" in subject)
- Create audit trail for compliance

**Regex Examples:**

**Match emails with "Invoice" in subject:**

```
Subject Regex: Invoice
Content Regex: (leave empty or .*)
```

**Match emails containing specific order numbers in body:**

```
Subject Regex: .*
Content Regex: ORDER-\d{6}
```

**Match urgent emails:**

```
Subject Regex: (?i)(urgent|critical|emergency)
Content Regex: .*
```

**Configuration Example:**

```
Type: onEmailSend
Subject Regex: (?i)urgent
Content Regex: .*
Custom Code:
  setTicketValue("Priority", "High");
  sendMailTo(["manager@example.com"], "Urgent email detected", "Priority Escalation");
```

#### 5. onTicketOverdue

**Description:** Triggers on a scheduled basis (cron job) for tickets that exceed a specified overdue threshold.

**When it triggers:** At the specified interval (cron schedule), checks all tickets and triggers for those whose age exceeds the overdue threshold.

**Required Fields:**

- **Type**: `onTicketOverdue`
- **Interval**: Cron expression defining when to check (e.g., "0 _/6 _ \* \*" = every 6 hours)
- **Overdue**: Time threshold in milliseconds (e.g., 86400000 = 24 hours)
- **Custom Code**: JavaScript code to execute for each overdue ticket
- **Disabled**: Should be `false` for the cron job to run

**Important Notes:**

- Setting **Disabled** to `true` stops the cron job completely
- The listener runs as a scheduled background job
- Executes for each ticket that meets the overdue criteria

**Common Interval Values:**

- `* * * * *` - Every minute (testing only)
- `0 * * * *` - Every hour
- `0 */6 * * *` - Every 6 hours
- `0 0 * * *` - Daily at midnight
- `0 9 * * 1` - Every Monday at 9 AM

**Common Overdue Values:**

- 3600000 - 1 hour
- 86400000 - 24 hours (1 day)
- 604800000 - 7 days (1 week)

**Use Cases:**

- Send reminder notifications for overdue tickets
- Escalate tickets that haven't been updated
- Auto-update priority for old tickets
- Generate overdue reports

**Configuration Example:**

```
Type: onTicketOverdue
Interval: 0 9 * * *  (daily at 9 AM)
Overdue: 172800000  (48 hours)
Disabled: false
Custom Code:
  sendMailTo(
    [getTicketValue("AssignedTo")],
    "Ticket #" + getTicketValue("PunchItem") + " is overdue!",
    "Overdue Ticket Reminder"
  );
```

## Special Configuration Fields

### Default Flag

**Type:** Boolean (checkbox)
**Default Value:** false

**Purpose:** The **Default** flag transforms workflow-dependent listeners into workflow-independent (global) listeners.

**How it works:**

- **Default = false**: Listener only triggers for tickets assigned to the specified Workflow ID
- **Default = true**: Listener triggers for **ALL** tickets regardless of workflow assignment, making it globally applicable

**When to use Default = true:**

- Organization-wide automation that applies to all tickets
- Global monitoring and notifications
- Universal field updates based on conditions
- System-wide audit logging

**Examples:**

**Workflow-Specific Listener (Default = false):**

```
Type: onPunchlistDetail
Checked Field: Status
Checked Value: Closed
Default: false
Workflow ID: 5
Custom Code: sendMailTo(["team@example.com"], "Project X ticket closed", "Notification")
```

→ Only triggers for tickets with Workflow ID = 5

**Global Listener (Default = true):**

```
Type: onPunchlistDetail
Checked Field: Status
Checked Value: Closed
Default: true
Workflow ID: (can be null)
Custom Code: setTicketValue("ClosedDate", Date())
```

→ Triggers for **all tickets** when Status changes to "Closed", regardless of workflow

### Disabled Flag

**Type:** Boolean (checkbox)
**Default Value:** false

**Purpose:** The **Disabled** flag completely stops listener execution without deleting the configuration.

**How it works:**

- **Disabled = false**: Listener is active and will trigger when conditions are met
- **Disabled = true**: Listener is completely inactive and will **never** trigger, even if conditions are met

**Special behavior for onTicketOverdue:**

- Setting **Disabled = true** stops the associated cron job
- Setting **Disabled = false** starts/resumes the cron job

**When to use Disabled = true:**

- Temporarily pause automation during maintenance
- Disable problematic listeners while debugging
- Seasonal or conditional listeners (enable/disable as needed)
- Testing alternative implementations

**Best Practice:** Use Disabled instead of deleting listeners to preserve configuration for future use.

## Listeners Table

![The listeners table](media/admin_guide_listeners_table.gif)

The Listeners Table:

- Displays all configured event listeners in a tabular format
- Shows Type, Checked Field, Checked Value, Workflow, Default, Disabled, and other type-specific columns
- Provides an interface to add, edit, and delete listeners
- Integrates with punch workflows and punch details

## Managing Event Listeners

### Add a Listener

1. Click the **Add Row (+)** button above the table

2. **Select the Type** from the dropdown (required first step)

3. Enter the **type-specific required fields:**
   - For **onWorkflowAccepted**: Checked Field (step number), Checked Value ("Accepted"), Workflow ID
   - For **onPunchlistDetail**: Checked Field (field name), Checked Value (trigger value), Workflow ID or Default
   - For **onTicketCreated**: Default (typically true)
   - For **onEmailSend**: Subject Regex and/or Content Regex
   - For **onTicketOverdue**: Interval (cron expression), Overdue (milliseconds)

4. **Configure special flags:**
   - **Default**: Check to make listener workflow-independent (applies globally)
   - **Disabled**: Check to temporarily disable the listener

5. **Write Custom Code:**
   - Click the Custom Code cell to open the code editor
   - Write JavaScript using available [Custom Code](#custom-code) functions
   - Test and save the code

6. Press **Enter** or click the **Save icon** at the end of the row to save the new listener

### Edit a Listener

1. Click the **Edit (pencil) icon** on the listener row

2. Modify any fields:
   - Change Type (note: required fields will change)
   - Update Checked Field/Value
   - Modify regex patterns for email listeners
   - Adjust Interval/Overdue for scheduled listeners
   - Toggle Default or Disabled flags
   - Update Custom Code

3. Click **Save** to apply changes

### Delete a Listener

1. Click the **Trash icon** on the listener row

2. Confirm deletion when prompted

**Note:** For onTicketOverdue listeners, deletion also stops the associated cron job.

### Test a Listener

**Before Activating:**

1. Configure the listener with Disabled = true
2. Test the custom code using the Custom Code editor
3. Verify the trigger conditions are correct
4. Set Disabled = false to activate

**Live Testing:**

1. Create or modify a ticket to match the trigger conditions
2. Verify the listener executes (check logs, email notifications, field updates)
3. Review any errors in the system logs

## Custom Code Integration

Event listeners utilize the full [Custom Code](#custom-code) API. When a listener triggers, the custom code executes with access to:

- All ticket fields via `getTicketValue()` and `setTicketValue()`
- Workflow control functions like `stepForwardJumpTo()`
- Communication functions like `sendMailTo()`
- File operations like `createPdfReport()`
- Utility functions like `Date()`

**Important:** Event listeners execute **after** the triggering event has already occurred and been saved to the database. They cannot prevent or block the event (unlike [Field Validators](#field-validators)).

## Comparison: Event Listeners vs Field Validators

| Feature                 | Event Listeners                             | Field Validators                 |
| ----------------------- | ------------------------------------------- | -------------------------------- |
| **Trigger Timing**      | After event occurs (post-save)              | Before save (immediate)          |
| **Blocking**            | Non-blocking (cannot prevent events)        | Blocks invalid changes           |
| **Purpose**             | React to changes, automate actions          | Prevent invalid data entry       |
| **User Feedback**       | Background execution, no immediate feedback | Immediate error message in toast |
| **Multi-field Support** | Single field per listener                   | Multiple fields per validator    |
| **Execution Context**   | Full Custom Code API                        | Validation-specific helpers      |

**When to use Event Listeners:**

- Send notifications when conditions are met
- Auto-populate fields after changes
- Trigger external integrations
- Log events for audit trail
- Schedule periodic checks

**When to use Field Validators:**

- Enforce data format requirements
- Validate business rules before saving
- Prevent invalid field values
- Cross-field validation
- Role-based field restrictions

See [Field Validators](#field-validators) for detailed validation documentation.

## Best Practices

### Listener Design

- Use descriptive naming in Custom Code comments
- Test listeners thoroughly before deploying to production
- Use Default = true sparingly (only for truly global automation)
- Keep custom code focused and maintainable

### Performance

- Avoid heavy processing in frequently-triggered listeners
- For onTicketOverdue, choose appropriate intervals (avoid overly frequent checks)
- Consider using Disabled flag during high-load periods

### Error Handling

- Include error handling in custom code (try/catch blocks)
- Test edge cases (null values, missing fields, etc.)
- Monitor system logs for listener execution errors

### Regex for Email Listeners

- Test regex patterns before deploying
- Use case-insensitive matching when appropriate: `(?i)pattern`
- Escape special regex characters: `\.`, `\?`, `\+`, etc.
- Keep patterns under 500 characters

### Workflow Integration

- Document which workflows have listeners attached
- Coordinate with workflow designers to avoid conflicts
- Use Default flag consistently across related listeners

# Custom Code

The **Custom Code** feature enables administrators to write JavaScript code that executes custom logic and automation throughout the Toolkit application. This powerful feature is used in multiple components:

1. **Workflow Steps** - Execute actions when workflow steps are accepted or disapproved
2. **Email Message Templates** - Generate dynamic email content with ticket data
3. **Punchfield Mapping** - Transform attribute values during ticket creation
4. **Event Listeners** - Automate actions triggered by specific events (see [Event Listeners](#event-listeners))
5. **Field Validators** - Enforce custom validation rules on ticket fields (see [Field Validators](#field-validators))

## Security & Execution Environment

Custom code is run as JavaScript in a **secured sandbox environment** with the following protections:

- **Sandboxed Execution**: Restricts access to internal system functions and prevents malware execution
- **Rate Limiting**: 10 requests per minute per user to prevent abuse
- **Code Length Limit**: Maximum 10,000 characters per code block
- **Dangerous Pattern Detection**: Automatically blocks code containing SQL injection attempts, `eval()`, process access, and other security risks
- **Server-Side Execution**: All custom code runs on the server, never exposing sensitive operations to the client

## Available Objects

Custom code has access to the following predefined objects:

- **`ActionResult`**: Enum containing workflow action results

  ```javascript
  ActionResult = {
    Yes: "Yes",
    No: "No",
    Undefined: "Undefined",
  };
  ```

- **`userAction.Result`**: Contains the result of the current workflow step action (ActionResult value)

- **`language`**: String containing the current user's language code (e.g., "en-US", "ko")

**Usage example:**

```javascript
if (userAction.Result === ActionResult.Yes) {
  console.log("The step was approved");
  setStatus("Approved");
} else {
  console.log("The step was disapproved");
  setStatus("Rejected");
}
```

## Available Functions

### Ticket Management Functions

- **`getPunchlistDetails()`**
  Returns an object containing all ticket field names and their current values.

  ```javascript
  const details = getPunchlistDetails();
  console.log(details["Status"]); // "Open"
  ```

- **`updatePunchlistDetails(data: Map<string, string>[])`**
  Updates multiple ticket fields with new values. Takes an array of objects with `Detail` (field name) and `Value` properties.

  ```javascript
  updatePunchlistDetails([
    { Detail: "Status", Value: "In Progress" },
    { Detail: "Assigned To", Value: "John Doe" },
  ]);
  ```

- **`getTicketValue(fieldName: string)`**
  Returns the current value of a specific ticket field.

  ```javascript
  const currentStatus = getTicketValue("Status");
  ```

- **`setTicketValue(fieldName: string, value: any)`**
  Sets the value of a specific ticket field.

  ```javascript
  setTicketValue("Priority", "High");
  setTicketValue("Due Date", Date("yyyy-MM-dd"));
  ```

- **`createHiddenField(detail: any)`**
  Creates a field that is specific only to the current ticket item (not visible in field configuration).

  ```javascript
  createHiddenField({ Detail: "InternalNotes", Value: "Auto-generated" });
  ```

- **`setStatus(status: string)`**
  Sets the status of the current ticket to the specified value.

  ```javascript
  setStatus("Closed");
  ```

- **`closeWorkflow()`**
  Closes the current workflow and sets the ticket status to "Closed".

  ```javascript
  if (userAction.Result === ActionResult.Yes) {
    closeWorkflow();
  }
  ```

### Workflow Control Functions

- **`stepForwardJumpTo(stepNumber: number, message?: string)`**
  Jumps to the specified workflow step number as a success/forward action. If a message is provided, it is printed to the activity chat. **This function overrides the Forward Step ID column value.**

  ```javascript
  // Jump to step 5 with a message
  stepForwardJumpTo(5, "Skipping steps 2-4 - auto-approved");
  ```

  > **Note:** This function has priority over the default Forward Step ID column when utilized for [workflows](#4-workflow-columns).

- **`stepBackwardJumpTo(stepNumber: number, message?: string)`**
  Jumps to the specified workflow step number as a failure/backward action. If a message is provided, it is printed to the activity chat. **This function overrides the Backward Step ID column value.**

  ```javascript
  // Jump back to step 1 with a message
  stepBackwardJumpTo(1, "Additional information required");
  ```

  > **Note:** This function has priority over the default Backward Step ID column when utilized for [workflows](#4-workflow-columns).

### Communication Functions

- **`sendMailTo(recipients: string[], message: string, subject: string, attachments?: any[])`**
  Sends an email to the specified recipients with optional attachments.

  ```javascript
  sendMailTo(
    ["user@example.com", "manager@example.com"],
    "Ticket #" + getTicketValue("PunchItem") + " has been approved.",
    "Ticket Approval Notification",
  );

  // With attachments
  sendMailTo(
    ["user@example.com"],
    "Please see attached report.",
    "Weekly Report",
    [reportPdfBuffer],
  );
  ```

- **`createComment(message: string)`**
  Creates a comment on the current ticket.

  ```javascript
  createComment("Automatically updated by workflow step 3");
  ```

### File Operations Functions

- **`createPdfReport(templateName?: string, ...params)`**
  Generates a PDF report using the configured template or specified template name. Additional parameters can be passed to customize the report.

  ```javascript
  const reportBuffer = createPdfReport();
  // Or with specific template
  const customReport = createPdfReport("CustomTemplate", {
    includeImages: true,
  });
  ```

- **`extractUpvf(fileName?: string)`**
  Extracts a UPVF file as a buffer. If no fileName is provided, extracts the ticket's associated UPVF file.

  ```javascript
  const upvfBuffer = extractUpvf();
  // Or specific file
  const specificFile = extractUpvf("project-model-v2.upvf");
  ```

- **`deleteUpvf(fileID: number)`**
  Deletes a UPVF file by its ID.

  ```javascript
  deleteUpvf(123);
  ```

### Utility Functions

- **`Date(formatString?: string)`**
  Returns the current date formatted according to the specified format string. Default format is "yyyy-MM-dd".

  ```javascript
  setTicketValue("CreatedDate", Date()); // "2025-12-01"
  setTicketValue("Timestamp", Date("yyyy-MM-dd HH:mm:ss")); // "2025-12-01 14:30:00"
  ```

## Practical Examples

### Example 1: Auto-populate fields based on status change

```javascript
// When ticket is marked as "Closed", auto-populate closure date and notify team
if (getTicketValue("Status") === "Closed") {
  setTicketValue("ClosedDate", Date("yyyy-MM-dd"));
  setTicketValue("ClosedBy", language === "en-US" ? "System" : "システム");

  sendMailTo(
    ["team@example.com"],
    "Ticket #" + getTicketValue("PunchItem") + " has been closed.",
    "Ticket Closure Notification",
  );
}
```

### Example 2: Conditional workflow jumps

```javascript
// If priority is "Critical", skip review step and go directly to approval
const priority = getTicketValue("Priority");
if (userAction.Result === ActionResult.Yes && priority === "Critical") {
  stepForwardJumpTo(5, "Critical priority - skipping review step");
} else if (userAction.Result === ActionResult.Yes) {
  stepForwardJumpTo(3, "Moving to standard review");
}
```

### Example 3: Cross-field updates

```javascript
// When Area is selected, auto-populate System based on mapping
const area = getTicketValue("Area");
const areaSystemMap = {
  "Engine Room": "Propulsion",
  Bridge: "Navigation",
  "Cargo Hold": "Cargo Handling",
};

if (areaSystemMap[area]) {
  setTicketValue("System", areaSystemMap[area]);
}
```

### Example 4: Date calculations and validations

```javascript
// Calculate and set due date to 7 days from now
const today = new Date();
const dueDate = new Date(today.setDate(today.getDate() + 7));
const formattedDueDate = dueDate.toISOString().split("T")[0];
setTicketValue("DueDate", formattedDueDate);

// Notify if ticket is overdue
const currentDueDate = new Date(getTicketValue("DueDate"));
if (currentDueDate < new Date() && getTicketValue("Status") !== "Closed") {
  sendMailTo(
    [getTicketValue("AssignedTo")],
    "Ticket #" + getTicketValue("PunchItem") + " is overdue!",
    "OVERDUE: Action Required",
  );
}
```

### Example 5: Multi-language support

```javascript
// Send notifications in user's language
const statusMessage =
  language === "ko" ? "티켓이 승인되었습니다" : "Ticket has been approved";

createComment(statusMessage);
sendMailTo(
  [getTicketValue("AssignedTo")],
  statusMessage,
  language === "ko" ? "티켓 승인" : "Ticket Approval",
);
```

# Field Validators

The **Field Validators** section provides administrators with powerful tools to enforce custom business rules and data validation on ticket fields. Field validators trigger **immediately when ticket detail values change**, validating the new value **before** it is saved to the database.

Unlike [Event Listeners](#event-listeners) which **react** to changes after they occur, field validators **prevent invalid data** from being saved in the first place. When a validator fails, the user receives an immediate error message via toast notification, and the field retains its previous value.

Field validators use [Custom Code](#custom-code) with specialized validation functions to enforce rules such as:

- Data format requirements (uppercase, email format, regex patterns)
- Business logic validation (date ranges, conditional requirements)
- Cross-field dependencies (multi-field validation)
- Role-based field restrictions (admins only, etc.)

## Accessing Field Validators

To manage field validators:

1. Navigate to **Settings → Workflows → Validators** in the administration menu
2. Select the workflow you want to configure from the dropdown
3. Field validators are workflow-specific - they only apply when the selected workflow is active on a ticket

## Key Differences from Event Listeners

| Feature                 | Field Validators                                 | Event Listeners                                   |
| ----------------------- | ------------------------------------------------ | ------------------------------------------------- |
| **Trigger Timing**      | Before save (immediate, on field change)         | After save (event-driven)                         |
| **Blocking**            | **Blocks invalid changes** - prevents save       | Non-blocking - cannot prevent events              |
| **Purpose**             | **Prevent** invalid data from entering system    | **React** to data changes with automation         |
| **User Feedback**       | Immediate error message in toast notification    | Background execution, no immediate feedback       |
| **Multi-field Support** | Yes - one validator can validate multiple fields | Single field per listener                         |
| **Execution Context**   | Validation-specific helper functions             | Full Custom Code API                              |
| **Workflow Scope**      | Workflow-specific (only when workflow is active) | Can be workflow-specific or global (Default flag) |

**When to use Field Validators:**

- Enforce data format requirements (uppercase, email, phone numbers)
- Validate business rules before saving (end date > start date)
- Prevent invalid field values (negative numbers, invalid statuses)
- Cross-field validation (if A then B is required)
- Role-based field restrictions (only admins can set certain values)

**When to use Event Listeners:**

- Send notifications when conditions are met
- Auto-populate fields after changes
- Trigger external integrations or workflows
- Log events for audit trail
- Schedule periodic checks

## Validator Configuration

### Validator Fields

When creating or editing a field validator, you configure the following:

- **Workflow** - Select which workflow this validator applies to (required - validators are workflow-scoped)
- **Fields** - Multi-select TagBox to choose one or more fields to validate
  - **Single-field validation**: Select one field (e.g., "Description")
  - **Cross-field validation**: Select multiple fields (e.g., "Start Date" and "End Date")
  - When multiple fields are selected, the validator runs whenever ANY of those fields change
- **Error Message** - The message shown to the user when validation fails (max 500 characters)
  - Be specific and actionable (e.g., "End Date must be after Start Date")
- **Active** - Toggle to enable/disable the validator without deleting it
- **Order** - Execution order (drag-and-drop to reorder)
  - Validators execute in ascending order
  - Lower numbers execute first
  - If any validator fails, subsequent validators still run (all errors are collected)
- **Custom Code** - JavaScript validation logic that returns `true` (allow) or `false` (block)

### Validator Execution Flow

When a user changes a ticket field value:

1. **Check for Active Workflows**: System checks if the ticket has any active (non-closed) workflows assigned
2. **Find Applicable Validators**: For each active workflow, system fetches all **Active** validators that include the changed field
3. **Execute in Order**: Validators execute in ascending Order
4. **Collect Errors**: If any validator returns `false`, its ErrorMessage is added to the error list
5. **Block or Allow**:
   - **All validators pass** (`return true`): Change is saved to database
   - **Any validator fails** (`return false`): Change is **BLOCKED**, error messages shown in toast, field retains old value

## Writing Validation Code

Field validator custom code must return a boolean value:

- **`return true`** → Validation **PASSES** (allow save)
- **`return false`** → Validation **FAILS** (block save, show ErrorMessage)

### Validation-Specific Helper Functions

Field validators have access to specialized functions for validation context:

#### `getCurrentFieldValue()`

**Returns:** The value being validated (the new value the user is trying to save)

**Usage:**

```javascript
// Ensure Description field is uppercase
return getCurrentFieldValue() === getCurrentFieldValue().toUpperCase();
```

#### `getFieldValue(fieldName: string)`

**Args:** `fieldName` - Name of any field in the ticket
**Returns:** Current value of the specified field

**Usage:**

```javascript
// Cross-field validation: End Date must be after Start Date
const startDate = new Date(getFieldValue("Start Date"));
const endDate = new Date(getCurrentFieldValue());
return endDate >= startDate;
```

#### `getAllFields()`

**Returns:** Object containing all field names and their current values

**Usage:**

```javascript
// Multi-field business logic
const fields = getAllFields();
if (fields["Status"] === "Closed" && !fields["Resolution"]) {
  return false; // Error: "Resolution required for closed tickets"
}
return true;
```

#### `hasRole(roleName: string)`

**Args:** `roleName` - Name of the role to check
**Returns:** `true` if current user has the specified role, `false` otherwise

**Usage:**

```javascript
// Only admins can set Priority to "Critical"
if (getCurrentFieldValue() === "Critical") {
  return hasRole("Admin");
}
return true;
```

### Available Context Variables

In addition to helper functions, the following variables are available directly in validation code:

- **`currentValue`** - The field value being validated (same as `getCurrentFieldValue()`)
- **`fieldName`** - Name of the field being validated (e.g., "Description", "Status")
- **`punchDetails`** - Object with ALL field values for the ticket (same as `getAllFields()`)
- **`userRoles`** - Array of current user's role names (e.g., `["Admin", "Editor"]`)

## Validation Examples

### Example 1: Format Validation - Uppercase Only

Ensure a field only contains uppercase letters:

```javascript
// Validation code for "Description" field
return getCurrentFieldValue() === getCurrentFieldValue().toUpperCase();
```

**Error Message:** "Description must be in uppercase letters only"

### Example 2: Format Validation - Email Format

Validate email address format:

```javascript
// Validation code for "Email" field
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(getCurrentFieldValue());
```

**Error Message:** "Please enter a valid email address"

### Example 3: Cross-Field Date Validation

Ensure End Date is after Start Date:

```javascript
// Validation code for "Start Date" and "End Date" fields (both selected)
if (fieldName === "End Date") {
  const startDate = new Date(getFieldValue("Start Date"));
  const endDate = new Date(getCurrentFieldValue());
  return endDate >= startDate;
}
return true; // For Start Date changes, allow (End Date validator will catch issues)
```

**Error Message:** "End Date must be on or after Start Date"

### Example 4: Role-Based Validation

Only admins can set priority to "Critical":

```javascript
// Validation code for "Priority" field
if (getCurrentFieldValue() === "Critical") {
  return hasRole("Admin");
}
return true; // Allow all other priority values
```

**Error Message:** "Only administrators can set Priority to Critical"

### Example 5: Conditional Required Field

Resolution is required when Status is "Closed":

```javascript
// Validation code for "Status" and "Resolution" fields (both selected)
const fields = getAllFields();

if (fieldName === "Status" && getCurrentFieldValue() === "Closed") {
  // When closing, Resolution must be filled
  return fields["Resolution"] && fields["Resolution"].trim() !== "";
}

if (fieldName === "Resolution" && fields["Status"] === "Closed") {
  // When status is already closed, Resolution cannot be empty
  return getCurrentFieldValue() && getCurrentFieldValue().trim() !== "";
}

return true;
```

**Error Message:** "Resolution is required when Status is Closed"

### Example 6: Numeric Range Validation

Ensure Budget is within acceptable range:

```javascript
// Validation code for "Budget" field
const budget = parseFloat(getCurrentFieldValue());

// Check for valid number
if (isNaN(budget)) {
  return false;
}

// Check range
return budget >= 0 && budget <= 1000000;
```

**Error Message:** "Budget must be between 0 and 1,000,000"

### Example 7: Complex Multi-Field Business Logic

Validate project milestone dates and dependencies:

```javascript
// Validation code for "Phase Start", "Phase End", "Project Status" fields
const fields = getAllFields();
const phaseStart = new Date(getFieldValue("Phase Start"));
const phaseEnd = new Date(getFieldValue("Phase End"));
const projectStatus = getFieldValue("Project Status");

// Validate date range
if (fieldName === "Phase End" || fieldName === "Phase Start") {
  if (phaseEnd <= phaseStart) {
    return false; // End must be after start
  }
}

// Cannot set status to "In Progress" without phase dates
if (
  fieldName === "Project Status" &&
  getCurrentFieldValue() === "In Progress"
) {
  if (!fields["Phase Start"] || !fields["Phase End"]) {
    return false;
  }
}

return true;
```

**Error Message:** "Phase End must be after Phase Start, and phase dates are required before starting the project"

## Management Actions

### Add a Validator

1. Navigate to **Settings → Workflows → Validators**
2. Select the target workflow from the dropdown
3. Click the **Add** button (plus icon)
4. **Configure the validator**:
   - **Fields**: Click the TagBox and select one or more fields to validate
   - **Error Message**: Enter a clear, specific error message
   - **Active**: Check to enable (default: true)
   - **Order**: Set execution order (default: 0)
   - **Custom Code**: Click to open the integrated code editor
5. **Write validation code**:
   - Write JavaScript code that returns `true` (pass) or `false` (fail)
   - Use helper functions: `getCurrentFieldValue()`, `getFieldValue()`, `getAllFields()`, `hasRole()`
6. **Test the code**:
   - Click **Compile** button to check syntax errors
   - Click **Test** button to run with sample data
   - Review any error messages
7. Click **Save** in the code editor
8. Click **Save** in the validator form to persist to database

### Edit a Validator

1. Locate the validator in the table
2. Double-click the row OR click the **Edit (pencil)** icon
3. Modify any fields:
   - Add/remove fields from the TagBox
   - Update error message
   - Toggle Active status
   - Change Order
   - Update Custom Code
4. Re-test if code was changed (Compile/Test buttons)
5. Click **Save** to apply changes

### Delete a Validator

1. Locate the validator in the table
2. Click the **Trash** icon at the end of the row
3. Confirm deletion in the dialog

**Warning:** Deleted validators cannot be recovered. Consider using **Active = false** instead to preserve configuration.

### Reorder Validators

Execution order matters - validators execute in ascending Order:

1. Locate the **six-dot handle** (⋮⋮) at the left of each validator row
2. Click and drag the handle up or down
3. Release to drop the validator in the new position
4. The Order field updates automatically
5. Validators now execute in the new order

**Best Practice:** Order validators from simple to complex:

- Simple format checks first (fail fast)
- Cross-field validations next
- Complex business logic last

## Testing Validators

### Using the Integrated Code Editor

Before activating a validator, test it thoroughly:

1. **Syntax Check**:
   - Click the **Compile** button
   - Review any syntax errors highlighted in red
   - Fix errors and compile again

2. **Test with Sample Data**:
   - Click the **Test** button
   - The editor runs your code with sample field values
   - Review the console output
   - Verify return value is boolean (`true` or `false`)

3. **Review Error Messages**:
   - Ensure your Error Message is clear and actionable
   - Test with invalid data to see the message

### Live Testing

After saving a validator, test it with real tickets:

1. **Create or open a test ticket** that has the configured workflow assigned
2. **Try to enter an invalid value** in the validated field
3. **Verify the behavior**:
   - Error message appears in toast notification at top of screen
   - Toast is red/error styled
   - Field retains its old value (change is blocked)
   - You cannot save the invalid value

4. **Try entering a valid value**:
   - No error message appears
   - Field updates successfully
   - Value is saved to database

5. **Test edge cases**:
   - Empty values
   - Null values
   - Boundary values (min/max)
   - Special characters
   - Very long strings

### Debugging Validators

If a validator isn't working as expected:

1. **Check the basics**:
   - Validator is **Active** (checkbox checked)
   - Workflow is assigned to the ticket
   - Field name matches exactly (case-sensitive)
   - Field is included in the validator's Fields list

2. **Review execution order**:
   - Earlier validators might be blocking before yours runs
   - Check Order values and execution sequence

3. **Test the code in isolation**:
   - Use Compile button to check syntax
   - Use Test button with sample data
   - Add `console.log()` statements for debugging
   - Check browser console for runtime errors

4. **Verify field names**:
   - Field names in `getFieldValue('FieldName')` must match exactly
   - Check field configuration in **Punchlist Fields** section
   - Names are case-sensitive

5. **Test with simple code first**:

```javascript
// Simplest test - always fail
return false;
```

If this doesn't show an error, there's a configuration issue (not a code issue)

## Best Practices

### Error Messages

- **Be specific**: "End Date must be after Start Date" not "Invalid date"
- **Be actionable**: Tell users what they need to fix
- **Use examples**: "Format: ABC-12345" or "Must be uppercase"
- **Keep it concise**: Under 200 characters when possible
- **Avoid technical jargon**: Write for end users, not developers

### Validation Code

- **Keep it simple**: Complex logic is harder to maintain and debug
- **Fail fast**: Check simple conditions first (null, empty, format)
- **Handle edge cases**: Check for null, undefined, empty strings
- **Use consistent return values**: Always return boolean, never undefined
- **Comment your code**: Explain business rules for future maintainers
- **Test thoroughly**: Test with valid, invalid, null, and boundary values

### Multi-Field Validators

- **Be explicit**: Use `fieldName` to check which field triggered the validation
- **Validate both directions**: If validating A against B, handle changes to both A and B
- **Consider order**: Validators run for all included fields

### Performance

- **Avoid heavy operations**: Validators run on every field change
- **Don't call external APIs**: Validators must be fast and synchronous
- **Minimize getAllFields() calls**: Call once and store result if needed multiple times
- **Consider validator count**: Too many validators can slow down field updates

### Organization

- **Use descriptive Order values**: 10, 20, 30 instead of 1, 2, 3 (easier to insert new ones)
- **Group related validators**: Use Order to keep related validations together
- **Document complex logic**: Add comments explaining business rules
- **Review regularly**: Outdated validators should be deactivated or deleted

### Security

- **Never trust client data**: Always validate on the server (validators run server-side ✓)
- **Use role-based validation**: Restrict sensitive fields with `hasRole()`
- **Validate format strictly**: Use regex for formats like email, phone, IDs
- **Prevent injection**: Don't use `eval()` or construct SQL queries in validators

## Troubleshooting

### Validator Not Triggering

**Problem:** Validator doesn't execute when field changes

**Solutions:**

1. Verify validator is **Active** (checkbox checked)
2. Check that the workflow is assigned to the ticket
3. Confirm workflow is not **Closed** (validators only run on active workflows)
4. Verify field name matches exactly (case-sensitive)
5. Check that field is in the validator's Fields list
6. Review Order - try setting to 0 to execute first

### Custom Code Errors

**Problem:** Validator code throws runtime errors

**Solutions:**

1. Use **Compile** button to check syntax errors
2. **Test** with sample data before saving
3. Check browser console for detailed error messages
4. Verify field names in `getFieldValue()` calls are correct
5. Handle null/undefined values:

```javascript
const value = getCurrentFieldValue();
if (!value) return false; // Require non-empty
```

6. Ensure you always return a boolean (`true` or `false`)

### Validation Too Strict

**Problem:** Validator blocks valid data

**Solutions:**

1. Review validation logic - may be too restrictive
2. Test edge cases (empty, null, special characters)
3. Consider role-based exceptions using `hasRole()`
4. Update error message to clarify requirements
5. Add logging to understand what values are being rejected:

```javascript
console.log("Validating:", getCurrentFieldValue());
```

### Validation Too Loose

**Problem:** Validator allows invalid data

**Solutions:**

1. Review validation logic - may have loopholes
2. Test with known invalid values
3. Check for missing null/empty checks
4. Ensure you're returning `false` for invalid cases
5. Verify regex patterns match your requirements

### Cross-Field Validation Issues

**Problem:** Multi-field validation not working correctly

**Solutions:**

1. Check which field triggered with `fieldName` variable
2. Ensure both fields are included in the Fields list
3. Handle validation for each field explicitly:

```javascript
if (fieldName === "StartDate") {
  // Validate start date logic
}
if (fieldName === "EndDate") {
  // Validate end date logic
}
```

4. Use `getFieldValue()` to get the other field's value
5. Consider creating separate validators for each field

### Performance Issues

**Problem:** Field updates are slow when validators are active

**Solutions:**

1. Reduce number of validators
2. Simplify complex validation logic
3. Remove unnecessary `getAllFields()` calls
4. Avoid loops and recursion in validation code
5. Consider combining multiple simple validators into one
6. Use **Disabled** flag to temporarily pause validators during bulk updates

## API Reference

For detailed information about all available Custom Code functions, see the [Custom Code](#custom-code) section.

**Validation-specific functions:**

- `getCurrentFieldValue()` - Get value being validated
- `getFieldValue(fieldName)` - Get any field value
- `getAllFields()` - Get all field values
- `hasRole(roleName)` - Check user role

**Available variables:**

- `currentValue` - Value being validated
- `fieldName` - Field being validated
- `punchDetails` - All field values
- `userRoles` - User's roles array

# Comment Configuration

The **Comment Configuration** section allows administrators to customize how comments are automatically generated when creating punch items using the "**Create with comment**" option. This feature enables a standardized naming convention for comments by selecting which punchlist detail fields should be included in the generated comment description.

![Comment Generation](media/admin_guide_comment_generation.gif)

## Overview

- Comment Configuration controls the automatic generation of comment descriptions when punch items are created with associated comments.

- Administrators can select up to **three** mapped punchlist detail fields to include in the comment description.

- The selected fields determine how the comment will be named, following the format: "PI-[PunchID] : [Field1Value] : [Field2Value] : [Field3Value]".

- Only punchlist details that have been mapped in the **Attribute Mapping** section are available for selection.

- The feature includes a toggle to enable or disable automatic comment generation entirely.

## Features

### 1. **Mapped Details Selection**

The page displays all punchlist details that have been mapped to 3D attributes, 2D attributes, or custom code in the Attribute Mapping section.

- Each mapped detail is shown with its name and mapping information (3D attribute, 2D attribute, or code mapping).

- If no mapped details are available, the page prompts administrators to configure attribute mappings first.

### 2. **Field Selection for Comments**

Administrators select which fields to include in auto-generated comments:

- Click the **Check** button next to a detail to add it to the comment configuration.

- Up to **3 fields** can be selected at once.

- If you select a 4th field while 3 are already selected, the first selected field is automatically removed.

- The order in which fields are selected determines their appearance order in the generated comment.

- Fields can be deselected by clicking their **Check** button again.

### 3. **Comment Preview**

- The **Ticket Comment Preview** section shows a live preview of how the selected fields will appear in the comment.

- Fields are displayed in the format: "Field1-Field2-Field3", separated by hyphens.

- A counter shows how many fields are currently selected (e.g., "Selected: 2/3").

- If no fields are selected, the preview displays "No fields selected".

### 4. **Comment Generation Toggle**

The **Comment Generation Enabled** checkbox controls whether automatic comment creation is active:

- When enabled, punch items created with the "Create with comment" option will automatically generate comments using the configured field values.

- When disabled, no automatic comments will be generated regardless of field selection.

- This toggle updates immediately without requiring a save action.

# Components

The **Components** section allows administrators to customize the available attributes and control default column visibility for the components table in the main punchlist details (Components tab).

![Components Page](media/admin_guide_components_page.png)

### **Features**

1. All available model attributes are displayed in the **Available Model Attributes** List.
   Click an attribute to move it from the **Available Model Attributes** list to the **Selected Attributes** list.
   Attributes in the **Selected Attributes** list are available as columns in the main components table.
2. To remove an attribute, click it in the **Selected Attributes** list to move it back to the **Available Model Attributes** list.

3. Four columns are permanent and cannot be removed: Type, UID, Bounding Box, and Drawing Name.

4. Administrators also have the ability to set the default visibility of each selected attribute using checkboxes located at the end of each row.

5. The visibility of the permanent columns can also be toggled as needed.

6. This configuration is applied by default for a user accessing the application for the first time.

![Components Page Actions](media/admin_guide_components_actions.gif)

# Accessible Pages

Navigate to **Enabled Pages** in the Admin menu to control which routes are visible in the application, configure the default landing page users are sent to after login, and manage the order and visibility of punchlist detail tabs.

![Enabled Pages](media/admin_guide_enabled_pages.png)

## Toolkit Pages

The **Toolkit Pages** table lists every registered application route.

### **View and Edit Hidden Routes**

1. Specify the route in the first column as `/<route>` — e.g. `/clashes`
2. Toggle the **Enabled** checkbox in the second column to allow or block access to that route

### **Default Landing Page**

Each enabled page can be designated as the **Default Landing Page** — the first screen users are redirected to immediately after a successful login.

- The **Default Landing Page** column shows a checkbox for each page row
- Only **enabled** pages can be set as the default; the checkbox is greyed out for disabled pages
- Only one page can be the default at a time — selecting a new page automatically clears the previous one
- **Unsetting the default is not allowed directly.** If you want to change the default, click the new page's checkbox; the old one clears automatically. You cannot uncheck the current default without first selecting a different one — an attempt to do so shows a warning toast

**How it affects login:**
After authentication, the application redirects the user to the configured default landing page. If no default is set, the first enabled non-settings page is used instead. This means you can point all users directly to the Tickets list, the LoToTo master list, or any other registered route without any code changes.

## Punchlist Tabs Visibility and Order

1. In the **Punchlist Tabs** table, administrators can toggle the **Visible** checkbox to control which tabs appear in the punchlist details section
2. The order of the tabs can be adjusted by dragging and dropping rows using the six-dot handle to the left of each row

# Ticket Report Designer

The **Ticket Report Designer** page provides a powerful, fully-featured PDF template editor that allows administrators to create and customize professional reports for ticket items. Built on the open source PDFME playground editor, this tool offers complete control over report layout, fields, and styling.

![Ticket Report Designer](media/admin_guide_ticketreport.png)

## Overview

- Design custom PDF reports with a visual drag-and-drop interface
- Map ticket detail fields to report elements
- Support for text, images, tables, signatures, QR codes, and more
- Real-time preview of report layouts
- Save templates to the database for use when generating ticket reports
- Load and modify existing templates or start from scratch

## Features

### 1. **Visual Template Designer**

The embedded PDFME designer provides an intuitive interface for creating PDF report templates:

- **Drag-and-Drop Elements**: Add text fields, images, shapes, tables, barcodes, QR codes, and signature fields to your template
- **Real-Time Preview**: See exactly how your report will look as you design it
- **Page Management**: Create multi-page reports with different layouts per page
- **Precise Positioning**: Use pixel-perfect positioning and alignment tools
- **Zoom Controls**: Adjustable zoom with a draggable zoom bar for detailed editing

### 2. **Field Naming Convention**

To populate report fields with actual ticket data, use the following naming conventions in the field "Name" property:

#### Regular Ticket Fields

**Format**: `reportData.DetailName`

**Example**: `reportData.Tag No`

- References the "Tag No" field from the Ticket Fields configuration
- Automatically populates with the value from the ticket's details
- Works with any custom field defined in the **Punchlist Fields** admin section

#### Screenshot Images

**Format**: `Initial` or `Final`

- `Initial` — Displays the uploaded initial screenshot image
- `Final` — Displays the uploaded final screenshot image
- These fields should be added as image elements in the designer

**Important**: The field name must exactly match the Detail name as defined in the Punchlist Fields administration page (case-sensitive).

### 3. **Base PDF Management**

**Change Base PDF**

- Upload a PDF file to use as the base template for your reports
- The base PDF appears as a background, with dynamic fields overlaid on top
- Useful for incorporating pre-designed features
- **File Requirements**: Must be a valid PDF file

**How to use**:

1. Click **"Change Base PDF (must be PDF)"** in the top navigation
2. Select a PDF file from your computer
3. The PDF pages become the background for your template
4. Add dynamic fields on top of the base PDF

### 4. **Template Management**

#### Save Template

- Click the **"Save"** button to save your current template design to the database
- The saved template is used when generating reports for tickets
- A success notification confirms when the template is saved
- Templates persist across sessions and are available to all users

#### Load Template

- Upload a previously saved template JSON file
- Useful for importing templates created elsewhere or restoring backups
- **File Requirements**: Must be a valid JSON file in PDFME format

**How to use**:

1. Click **"Load Template (must be JSON)"**
2. Select a JSON template file
3. The designer loads the template for editing

#### Download Template

- Export your current template as a JSON file
- Use this to create backups or share templates
- Click **"DL Template"** to download

#### Reset Template

- Click the **"Reset"** button to clear the current template
- Returns to a blank template with default settings
- **Warning**: This action cannot be undone

## Troubleshooting

**Fields not populating with data**

- Verify the field name matches exactly (case-sensitive) with the Detail name in Punchlist Fields
- Ensure the naming convention `reportData.DetailName` is used correctly
- Check that the ticket has data in the specified field

**Base PDF not displaying**

- Confirm the uploaded file is a valid PDF format
- Check the PDF file size is reasonable (under 10MB recommended)
- Verify the PDF is not password-protected or encrypted

**Template not saving**

- Check browser console for error messages
- Ensure you have administrator permissions
- Verify database connectivity

# Power BI

The **Power BI** section lets administrators manage embedded reports. Reports are grouped by type and configured in-app without modifying environment variables.

## **1.  One‑Time Azure & Power BI Setup**

### Azure Setup for Power BI Embedding

Follow these 7 steps to configure your Azure and Power BI environment. Once complete, no additional Azure changes are required — everything else is managed inside the app.

| #   | Task                                   | What to Do                                                                                                                                                                                                                                     |
| --- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1   | **Register an Azure AD App**           | Go to **Azure Portal** → **Azure Active Directory** → **App registrations** → **New registration**.<br>Save the **Application (Client) ID** and **Directory (Tenant) ID**.<br>_No extra API permissions are needed._                           |
| 2   | **Generate a Client Secret**           | Go to **Certificates & Secrets** → **New client secret**.<br>Copy the value and store it securely (used as `POWERBI_SECRET`).                                                                                                                  |
| 3   | **Enable Tenant Settings**             | In **Power BI Service** → **Admin Portal** → **Tenant Settings**, enable:<br>▪ Embed content in apps<br>▪ Allow service principals to use Power BI APIs<br>_Scope = Entire organization or a security group containing the service principal._ |
| 4   | **Add Service Principal to Workspace** | In each target workspace → **Access** → **Add member** → search for the app name.<br>Set **Role** to `Member` or `Admin`.                                                                                                                      |
| 5   | **Publish Reports**                    | Open **Power BI Desktop** and publish your `.pbix` file to the same workspace.                                                                                                                                                                 |
| 6   | **Provision Embedded Capacity**        | Go to **Azure Portal** → **Power BI Embedded** → choose a capacity tier (e.g., A1).<br>In Power BI workspace: **Settings** → **Premium** → assign the capacity.                                                                                |
| 7   | **(If report connects to Azure SQL)**  | In **SQL Server** → **Networking** → enable “Allow Azure services and resources to access this server”.                                                                                                                                        |

---

Once these steps are completed, your Azure environment is ready. All further actions are handled within the app.

## **2.  Server‑Side Environment Variables**

- Only three secrets live in the deployment environment (never in the UI):

```json
POWERBI_TENANT_ID = <Directory / Tenant GUID>
POWERBI_CLIENT_ID = <Application / Client GUID>
POWERBI_SECRET = <Client Secret>
```

- The application reads them through:

```json
{
  authenticationMode: 'ServicePrincipal',
  authorityUrl: 'https://login.microsoftonline.com/',
  scopeBase: 'https://analysis.windows.net/powerbi/api/.default',
  tenantId:   process.env.POWERBI_TENANT_ID,
  clientId:   process.env.POWERBI_CLIENT_ID,
  clientSecret: process.env.POWERBI_SECRET,
}
```

- **Note**: Authentication Mode must remain ServicePrincipal – this is the Microsoft‑recommended approach for App‑Owns‑Data embeds and avoids user consent pop‑ups.

## **3. Adding & Maintaining Reports inside the App**

Use the Power BI UI page to:

## Report Management Actions

| **Action**                 | **Steps**                                                                                                                                                                                                                                                     |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Add a Report**           | 1. Navigate to **Admin › Power BI**.<br>2. Click **“+ Add Report”**.<br>3. Enter: **Report Name**, **Report ID**, **Workspace ID**, and **Type** (copy GUIDs from Power BI Service URL).<br>4. Click **Save** – the app validates IDs by contacting Power BI. |
| **Edit a Report**          | Click **✎ (edit)** → change values → **Save**.                                                                                                                                                                                                                |
| **Delete a Report**        | Click **🗑** on the appropriate report to delete the report.                                                                                                                                                                                                  |
| **Create a Report Type**   | Type a new value into the Report Type field when adding a report — it will be created automatically.                                                                                                                                                          |
| **Delete a Report Type**   | Click **🗑** next to a type to remove it and all its reports.\_                                                                                                                                                                                               |
| **Hide / Show the Navbar** | Click **«** or **»** to collapse the left pane and view the report full‑width.                                                                                                                                                                                |

![Power BI Page](media/admin_guide_powerbi.gif)

# LOTO Administration

The **LOTO (Lockout/Tagout)** administration section enables administrators to configure templates, metadata fields, and catalogue items that define how Lockout/Tagout work programs operate throughout the application.

## Overview

The LOTO system provides a comprehensive framework for managing Lockout/Tagout work programs in industrial environments. Administrators configure:

- **Templates**: Reusable step sequences for different work program types
- **Metadata Fields**: Dynamic columns that appear in work program list tables and steps tables
- **Catalogue Items**: Placeable symbols and equipment types for P&ID drawings

Users leverage these configurations to create, execute, and track work programs with proper safety procedures and documentation.

## LOTO Templates

Navigate to **Settings → LOTO Settings** to manage reusable templates that define step sequences for work programs.

### What are LOTO Templates?

Templates are pre-configured sets of steps organized by phase (Preparation, De-commissioning, Repair, Commissioning) that can be applied to new work programs. They ensure consistency and best practices across similar work activities.

### Template Management Actions

#### **View All Templates**

The Templates page displays all available templates in a table format with the following columns:

- **Template Name**: Descriptive name for the template
- **Type**: Category or classification (e.g., "Maintenance", "Inspection")
- **Description**: Detailed explanation of the template's purpose
- **Category**: Grouping classification for organizing templates
- **Last Modified**: Date of last modification
- **Last Modified By**: User who last updated the template

#### **Add a New Template**

1. Click the **Add (+)** button in the toolbar
2. Enter the required fields:
   - **Template Name**: Unique identifier (required)
   - **Type**: Classification category
   - **Description**: Purpose and usage details
   - **Category**: Organizational grouping
3. Click **Save** to create the template
4. The template appears in the list and can now be opened to add steps

#### **Edit Template Properties**

1. Click the **Edit (pencil)** icon on the template row
2. Modify any field:
   - Template Name
   - Type
   - Description
   - Category
3. Click **Save** to apply changes

#### **Delete a Template**

1. Click the **Trash** icon on the template row
2. Confirm deletion in the dialog
3. **Warning**: Deleting a template does not affect work programs that were created using it. However, the template will no longer be available for new work programs.

#### **Configure Template Steps**

1. Double-click a template row OR click the template name
2. Navigate to the Template Steps page for that template

### Template Steps Configuration

After selecting a template, administrators can configure the steps for each phase.

#### **Phase Tabs**

Template steps are organized into four phases:

- **Preparation**: Initial setup and safety procedures
- **De-commissioning**: Shutdown and isolation procedures
- **Repair**: Maintenance or repair work activities
- **Commissioning**: Startup and restoration procedures

Click each tab to view and edit steps for that phase.

#### **Steps Table Columns**

| Column              | Description                                                                                                                                                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Order**           | Sequential number determining execution order (drag to reorder)                                                                                                                                                                      |
| **Step Title**      | Name of the step (required)                                                                                                                                                                                                          |
| **Step Type**       | Classification (e.g., "Safety", "Isolation", "Verification")                                                                                                                                                                         |
| **Dynamic Columns** | Additional columns defined via **LOTO Metadata Configuration** for the current phase (e.g., Responsible Party, Action Owner). These are configurable by administrators, and only fields with **Show in List** enabled are displayed. |
| **Comment**         | Additional notes, instructions, or requirements                                                                                                                                                                                      |
| **Interdependency** | Other steps (by Order number) that must be completed first                                                                                                                                                                           |

#### **Add a Template Step**

1. Select the phase tab (Preparation, De-commissioning, Repair, or Commissioning)
2. Click the **Add (+)** button
3. Enter step details:
   - **Step Title**: Clear, actionable description (required)
   - **Step Type**: Category or classification
   - **Dynamic fields**: Fill in any additional columns configured for this phase (e.g., Responsible Party, Action Owner). These are defined in **LOTO Metadata Configuration** with the corresponding phase as Item Type.
   - **Comment**: Detailed instructions or safety notes
   - **Interdependency**: Select prerequisite steps by Order number
4. Click **Save** or press Enter

#### **Edit a Template Step**

1. Click the **Edit (pencil)** icon on the step row
2. Modify any field
3. Click **Save** to apply changes

**Note**: Changes to template steps do NOT affect existing work programs that were created from this template. They only apply to newly created work programs.

#### **Delete a Template Step**

1. Click the **Trash** icon on the step row
2. Confirm deletion
3. The step is removed from the template

#### **Reorder Template Steps**

1. Locate the **six-dot handle** (⋮⋮) at the left of the row
2. Click and drag the step to the desired position
3. Release to drop
4. The Order numbers update automatically

#### **Set Step Interdependencies**

Interdependencies ensure that prerequisite steps must be completed before a step becomes available.

1. Edit the step
2. Click the **Interdependency** cell
3. Select one or more prerequisite steps by their Order number
4. Save the step

### Using Templates in Work Programs

When users create a new work program, they can import a template through the **Planning → Template** tab. The template steps are copied to the work program, including any dynamic field values defined on the template steps, where users can then execute them. Similarly, when cloning steps from an existing work program, dynamic field values are preserved in the copy.

**Key Point**: Template changes do not retroactively affect existing work programs. Only new work programs created after the template is modified will include the updates.

## LOTO Metadata Configuration

Navigate to **Settings → LOTO Metadata** to define the **dynamic columns (metadata fields)** that appear in work program tables. Fields can target either **list tables** (e.g., Blind List, Valves List, Electrical Drives List, Drains List, Hose List, Other List) or **steps tables** (Preparation, De-commissioning, Repair, Commissioning). These fields control what users can view and edit in each table.

### What you can do here

- **Add** new columns (fields) to specific lists.
- **Edit** field properties (labels, data source, input type, etc.).
- **Delete** fields you no longer want shown.
- **Reorder** fields by dragging rows to change the column order in the list tables.

## Field Reference

| Field                                | What it means / how it’s used                                                                                                                                                                                                                                           |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Field Name**                       | Internal identifier; must be unique, start with a letter, and use only letters, numbers, or underscores.                                                                                                                                                                |
| **Display Label**                    | The column header shown to users in list tables.                                                                                                                                                                                                                        |
| **Item Type**                        | Determines which table shows this field. **List types**: Blind List, Valves List, Electrical Drives List, Drains List, Hose List, Isolation Markup. **Phase types**: Preparation, De-commissioning, Repair, Commissioning (columns appear in the Steps table for that phase). |
| **Show in List**                     | Toggle (default: **on**). When disabled, the field is hidden from list/steps table columns without deleting the field definition or its stored values. Re-enable to show the column again.                                                                              |
| **Include in Summary**               | Toggle (default: **on**). When enabled, this field is included in the exported summary PDF report tables.                                                                                                                                                               |
| **Column Width**                     | The pixel width applied to this column in list and steps tables. Defaults to **150**. Editable for all fields including hardcoded ones.                                                                                                                                  |
| **Data Source**                      | Where the value comes from: **UserEntered** (users type/select values), **ConnectedItem** (auto-filled from P&ID object), or **Hardcoded** (built-in system columns that cannot be created or removed by administrators). You can filter by this value using the column header filter to view only hardcoded rows. |
| **Connected Item Attribute**         | Attribute to display when **Data Source = ConnectedItem**.                                                                                                                                                                                                              |
| **Input Type (UserEntered only)**    | Editor control for manual entry: **Text**, **Dropdown**, **Boolean**, **Number**, or **Date**.                                                                                                                                                                          |
| **Dropdown Options (Dropdown only)** | Allowed choices for **Input Type = Dropdown**; provide as a comma-separated list.                                                                                                                                                                                       |
| **Data Type**                        | Must match the input type (**String/Number/Boolean/Date**); enforced automatically.                                                                                                                                                                                     |

## Common Operations

![Common Metadata Operations](media/admin_guide_metadata_operations.gif)

### Add a Field

1. Click the **"+"** button.
2. Fill out:
   - **Field Name:** Internal name (letters/numbers/underscores; must start with a letter).
   - **Display Label:** The column header users will see.
   - **Item Type:** Which table the field appears in. Choose a **list type** (e.g., Blind List) to add the column to that list table, or a **phase** (e.g., Preparation, De-commissioning) to add the column to the steps table for that phase.
   - **Data Source:** **UserEntered** (manually entered in lists) or **ConnectedItem** (auto from connected P&ID object).
   - **Input Type (UserEntered only):** **Text**, **Dropdown**, **Boolean**, **Number**, or **Date**.
   - **Dropdown Options (Dropdown only):** Comma-separated choices (e.g., `Open,Closed,Locked`).
   - **Data Type:**
     - If **UserEntered** → auto-matched and locked to the Input Type:  
       **Text → String**, **Dropdown → String**, **Boolean → Boolean**, **Number → Number**, **Date → Date**.
     - If **ConnectedItem** → choose **String/Number/Boolean/Date** to match the source attribute.
   - **Connected Item Attribute (ConnectedItem only):** Attribute to pull from the P&ID object.
3. Click the **Save Icon** button. The field is added and positioned at the end of the list’s columns.

### Edit a Field

1. Click the **Edit (pencil)** icon on the row.
2. Adjust the properties.
   - When **Data Source = ConnectedItem**, **Input Type** and **Dropdown Options** are disabled.
   - When **Data Source = UserEntered**, **Connected Item Attribute** is disabled.
3. Click the **Save Icon** button to finalize changes.

### Hide a Field (Show in List)

If you want to temporarily remove a column from the user-facing tables without deleting the field definition:

1. Edit the field row.
2. Uncheck the **Show in List** toggle.
3. Save. The column is immediately hidden from list/steps tables.
4. Re-enable the toggle at any time to restore the column. All stored values are preserved.

### Delete a Field

1. Click the **Delete (trash)** icon and confirm.
   - The column **disappears from the UI** for users.
   - Existing values in the database are **not deleted**.
   - Re-create the same **Field Name** later to show the values again.

### Reorder Fields (Column Order)

1. Drag the **row handle** up or down.
2. Release to drop. The new order is saved and reflected in the list table layout.

---

## Rules & Validation (Quick Guide)

- **Required fields:** Field Name, Display Label, Item Type, Data Source, Data Type.
- **Dropdown Options:** Required if **Input Type = Dropdown**.
- **Type Matching:** **Data Type** must match the **Input Type** (checked automatically).
- **Connected vs UserEntered:** Only the relevant inputs are enabled; the others are dimmed and cannot be edited.

## What End Users Will See

- Fields with a **list Item Type** (e.g., Blind List, Valves List) appear as **columns** in the corresponding work program list tables.
- Fields with a **phase Item Type** (e.g., Preparation, De-commissioning) appear as **columns** in the steps table and template steps table for that phase.
- Only fields with **Show in List** enabled are displayed as columns. Disabling this toggle hides the column from users without deleting the field or its data.
- **UserEntered** fields can be **edited inline** by users (double-click cell, edit, press **Enter** to save).
- **ConnectedItem** fields are **read-only**, reflecting live data from the connected P&ID object.
- **Hardcoded** fields (Data Source = Hardcoded) are built-in system columns seeded automatically per list type. Each list type (Blind List, Valves List, Electrical Drives List, Drains List, Hose List, Isolation Markup) has its own independent set of hardcoded entries. These rows are visible in the metadata mapping grid and their **Column Width** can be adjusted, but they cannot be created, deleted, or have their Data Source changed by administrators.
- **Type** is a special hardcoded column that automatically displays the **Connected Item Name** (the name of the catalogue item placed on the P&ID).

# Catalogue

Use this page to manage the **placeable symbols/templates** that appear in the Sketching/Placement tools for work program lists (e.g., **Blinds, Valves, Electrical Drives, Drains, Hoses, Other**). Each catalogue item corresponds to a symbol (SVG) that users can place onto P&IDs when creating or updating work program items.

### What is the LOTO Catalogue?

The LOTO Catalogue contains the library of symbols and equipment types that users can place on P&ID drawings when building work programs. Each catalogue item represents a physical device (blind, valve, electrical drive, etc.) and includes:

- A visual symbol (SVG file) for placement on drawings
- Associated actions for commissioning and de-commissioning (for Blinds, Valves, and Electrical Drives only)
- Classification by list type

### Catalogue Management Actions

- **Add**, **Edit**, **Delete**, and **Reorder** catalogue items
- **Effect for users:** Items you configure here appear in the **Add list item** flow and Sketching Tool catalogue lists

### Field Reference

| Field                   | Description                                                                                                                                                  |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Catalogue Item Name** | Friendly name shown to users in placement dialogs (e.g., "SC-1 Spade Blind", "Gate Valve 2-inch")                                                            |
| **SVG Path**            | Path to the SVG symbol file in the model/catalogue folder (e.g., `IntelliPidCatalogue\LOTO_Symbols\Lototo_V2_w_ll.svg`)                                      |
| **List Type**           | Which work program list this item appears under: **Blind List**, **Valves List**, **Electrical Drives List**, **Drains List**, **Hose List**, **Other List** |
| **Display Order**       | Hidden system field updated when you drag rows; controls visible order in placement pickers                                                                  |
| **Com Action**          | Commissioning action description (Blinds, Valves, Electrical Drives only)                                                                                    |
| **DeCom Action**        | De-commissioning action description (Blinds, Valves, Electrical Drives only)                                                                                 |
| **Com Action Owner**    | Responsible party for commissioning (Blinds, Valves, Electrical Drives only)                                                                                 |
| **DeCom Action Owner**  | Responsible party for de-commissioning (Blinds, Valves, Electrical Drives only)                                                                              |

**Note**: Action fields (Com Action, DeCom Action, Com Action Owner, DeCom Action Owner) are only available for **Blind List**, **Valves List**, and **Electrical Drives List**. These fields are automatically disabled for Drains List, Hose List, and Other List.

### Common Operations

![Common Catalogue Operations](media/admin_guide_catalogue_operations.gif)

#### **Add a Catalogue Item**

1. Click the **Add (+)** button
2. Fill out the required fields:
   - **Catalogue Item Name**: The name users see when selecting an item to place (e.g., "SC-1 Spade Blind", "Gate Valve 2-inch")
   - **SVG Path**: The file path to the symbol (SVG) in your model/catalogue folder (e.g., `IntelliPidCatalogue\LOTO_Symbols\Lototo_V2_w_ll.svg`)
     - Ensure the SVG exists and is accessible in the deployment environment
   - **List Type**: Which work program list this item belongs to
3. (Optional) For Blinds, Valves, and Electrical Drives, enter:
   - **Com Action**: Description of commissioning action
   - **DeCom Action**: Description of de-commissioning action
   - **Com Action Owner**: Responsible party for commissioning
   - **DeCom Action Owner**: Responsible party for de-commissioning
4. Click the **Save** icon button
5. The new item appears for users in the corresponding placement catalogue

**Tip**: Use descriptive names that help users quickly identify the correct item (e.g., "Spectacle Blind 6-inch" instead of just "Blind").

#### **Edit a Catalogue Item**

1. Click the **Edit (pencil)** icon on the row
2. Update any field:
   - Name
   - SVG Path
   - List Type
   - Action fields (if applicable)
3. Click **Save** to finalize changes

**Note**: If you change the List Type from Blinds/Valves/Electrical Drives to Drains/Hose/Other, the action fields are automatically cleared.

#### **Delete a Catalogue Item**

1. Click the **Delete (trash)** icon on the row
2. Confirm deletion in the dialog

**Important**:

- The item is **removed from the placement catalogue** for users
- **Already-placed objects** using that SVG are **not deleted** from P&IDs or work programs
- Users can no longer select this item for new placements

#### **Reorder Catalogue Items**

1. Drag the **six-dot handle** (⋮⋮) up or down to change order
2. Release to drop
3. The **Display Order** updates automatically
4. Items appear in this order in the placement picker for users

**Best Practice**: Order items logically (e.g., group similar items together, or order by frequency of use).

### SVG Symbol Requirements

- **File Format**: Must be a valid SVG file
- **Location**: Must exist in the model/catalogue folder accessible by the application
- **Path Format**: Use backslashes for Windows paths (e.g., `IntelliPidCatalogue\LOTO_Symbols\symbol.svg`)

# LoToTo Print Summary Templates

The **Print Summary Templates** page lets administrators upload and manage `.xlsx` Excel files that are used as the layout template when generating the LoToTo Work Program Print Summary PDF. When a user clicks **Print all** on the Summary tab of a work program, the system checks for an active template and uses it to produce the PDF instead of the default built-in layout.

Navigate to **Settings → Print Summary Templates** in the admin menu.

![Print Summary Templates admin page](media/admin_guide_lototo_print_summary_templates.png)

## How It Works

1. An administrator uploads an `.xlsx` file that defines the PDF layout — headers, sections, labels, and field placeholders
2. The template is marked as **Active**
3. When any user generates a print summary PDF, the application fills the template with the work program's real data and produces a fully populated PDF
4. The PDF is then merged with the P&ID sketch PDFs captured from the model, with the template pages appearing first

Only one template can be active at a time. Uploading a new template does not automatically activate it — you must explicitly set it as active.

## Template Actions

| Action         | How                                                                                                                                                                 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Upload**     | Click **Upload File** → select an `.xlsx` file → file is uploaded to storage and registered in the database                                                         |
| **Set Active** | Click the checkbox in the **Active** column on any inactive row → that template becomes active, the previous active template is deactivated                         |
| **Preview**    | Click any row (not the delete or download button) → a popup opens rendering each sheet of the Excel file as HTML so you can inspect the layout before activating it |
| **Download**   | Click the download icon on a row → downloads the original `.xlsx` file                                                                                              |
| **Delete**     | Click the trash icon → confirmation dialog → removes the record from the database                                                                                   |

## Template Preview

Clicking a row opens a preview popup that renders the `.xlsx` file as HTML, faithfully reproducing:

- Merged cells, column widths, and row heights
- Font styling (name, size, bold, italic, colour)
- Cell background fills (solid colours, theme colours with tint)
- Borders (thin, medium, thick)
- Horizontal and vertical text alignment

If the template has multiple sheets, tab buttons appear at the top of the popup for switching between them.

## Designing a Template

Templates are standard Excel (`.xlsx`) files. The key feature is **field tokens** — any cell whose value is `#FieldName` is automatically replaced with the corresponding work program data when the PDF is generated.

### Field Tokens — `#FieldName` and `##FieldName`

Place a token like `#WorkProgramName` in any cell and it will be replaced at generation time. Tokens are case-insensitive.

Any metadata field names and master detail column names configured for the work program are also supported as tokens, using the same `#FieldName` syntax. If a token cannot be resolved, the raw token text is displayed as-is in the PDF.

There are two token formats with different rendering behaviour for dropdown-type metadata fields:

| Token format    | Behaviour for dropdown fields                                                                         | Behaviour for all other fields |
| --------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------ |
| `#FieldName`    | Selected value shown in the cell; all dropdown options listed as an interactive checklist below the row, with the selected option pre-checked | Value substituted inline       |
| `##FieldName`   | Selected value shown inline only — no checklist is generated                                          | Value substituted inline       |

Use `##FieldName` (double hash) when you want a dropdown field to appear as plain text in the PDF without the accompanying option checklist.

### Control Tokens

Two tokens have special rendering behaviour and are **not** replaced with data values:

| Token                                       | Effect in the generated PDF                                                                                          |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `#PIDSketches`                              | A list of all P&ID drawing names linked to the work program is printed below this cell                               |
| `#Comment` / `#Kommentar` / `#Comentariu`  | An interactive, editable text field is placed over the entire cell in the PDF — users can type into it inside a PDF reader |

All three comment tokens behave identically — use whichever matches your template language.

### Embedded Images

Any images embedded directly in the Excel template (inserted into cells via **Insert → Picture**) are rendered into the generated PDF at the position and size defined in the template. This allows logos, diagrams, icons, or any other static graphic to be included in the report layout without needing to be part of a data token.

### Interactive Fields

- **Boolean cells** — Any Excel cell containing a checkbox is rendered as an interactive checkbox in the PDF. Users can tick or untick it in a PDF reader
- **Dropdown fields (`#FieldName`)** — When a single-hash token maps to a dropdown-type metadata field, the selected value is shown in the cell and all available options are listed as an interactive checklist below the row, with the selected option pre-checked
- **Dropdown fields (`##FieldName`)** — When a double-hash token maps to a dropdown-type metadata field, only the selected value is shown inline — no checklist is generated
- **`#Comment` / `#Kommentar` / `#Comentariu` cells** — Rendered as fillable text fields sized to the full cell dimensions

### Sheet Naming

Name your Excel sheets to control what data is injected into each one:

| Sheet name keyword                       | Data injected                                            |
| ---------------------------------------- | -------------------------------------------------------- |
| Contains `valve`                         | Valves list                                              |
| Contains `blind`                         | Blind list                                               |
| Contains `electrical`, `edrive`, `drive` | Electrical drives list                                   |
| Contains `drain`                         | Drains list                                              |
| Contains `hose`                          | Hose list                                                |
| Contains `other`                         | Other list                                               |
| Contains `preparation`                   | Preparation steps table                                  |
| Contains `decommissioning` or `decom`    | De-commissioning steps table                             |
| Contains `repair`                        | Repair steps table                                       |
| Contains `commissioning`                 | Commissioning steps table                                |
| Any other name                           | General template sheet — field tokens only, no list data |

Steps data is always rendered on any sheet named **Work Program** regardless of whether it matches a steps keyword.

Sheets are rendered in their Excel tab order. All other Excel formatting (colours, fonts, borders, merged cells) is preserved in the generated PDF.

# Languages

The **Languages** page allows administrators to manage all language translations used throughout the application. Administrators can download all current translations as an Excel file, edit them offline, upload updated or new languages via Excel, edit individual translations in the grid, and configure default language settings.

![The languages administration page](media/admin_guide_languages_page.png)

## Overview

- The Language Management system uses a **two-table architecture** in the database:
  - **StandardTranslations**: Base translations loaded from the `translations.json` file at application startup. These serve as read-only templates and cannot be edited through the UI.
  - **CustomTranslations**: User-editable translation overrides that take priority over standard translations. These are managed entirely through the admin interface.

- Both tables share the same schema: each row has a `key` (the translation key) and a `translations` column containing a JSON object mapping language codes to translated values. For example:

  ```json
  { "en-US": "Add Language", "ko": "언어 추가", "ar": "إضافة لغة" }
  ```

- Administrators can set a **default language** that serves as the fallback for all translations. This setting is stored in the `Dictionary` table under the `SYSTEM_CONFIG` language with the key `DEFAULT_LANGUAGE`.

- Users can select their preferred language from available languages, which persists across sessions via the `Language` field on their account record. Users may also select "Default" to always follow the system default language.

- Translation priority system ensures the best available translation is displayed:
  1. Custom translation in current language (highest priority)
  2. Standard translation in current language
  3. Custom translation in default language
  4. Standard translation in default language
  5. `en-US` as the ultimate hardcoded fallback
  6. `(Missing: <key>)` placeholder if no translation exists anywhere

## Standard Translations and Startup Initialization

Standard translations are the baseline dictionary provided with the application. They originate from a `translations.xlsx` Excel file that is converted to `translations.json` using a build-time conversion script.

### How Standard Translations Are Loaded

1. **Build time**: The `translations.xlsx` file (a single-sheet workbook with columns `key`, `en-US`, `ko`, `ar`, `de-DE`, etc.) is converted to `translations.json` using the conversion script:

   ```bash
   npx ts-node scripts/convertTranslationsToJson.ts translations.xlsx translations.json
   ```

   The resulting JSON has this structure:

   ```json
   {
     "Add Language": { "en-US": "Add Language", "ko": "언어 추가" },
     "Cancel": { "en-US": "Cancel", "ko": "취소" }
   }
   ```

2. **Application startup**: The `start.js` entry point starts the server, waits for it to be healthy, then calls the `/api/init-translations` endpoint. This endpoint invokes `initializeStandardTranslations()`, which reads `translations.json` from the project root, clears all existing rows in the `StandardTranslations` table, and inserts the JSON data. This means standard translations are always refreshed on every application restart.

3. **The `translations.xlsx` file** is also used at runtime by the admin page to read available standard languages (via `getLanguagesFromExcel()`), so both files should be kept in sync.

### Updating Standard Translations

To update standard translations:

1. Edit the `translations.xlsx` file in the project root directory.
2. Run the conversion script to regenerate `translations.json`:
   ```bash
   npx ts-node scripts/convertTranslationsToJson.ts translations.xlsx translations.json
   ```
3. Restart the application. The translations will be automatically loaded from `translations.json` at startup.

The conversion script also supports the reverse direction (JSON to Excel):

```bash
npx ts-node scripts/convertTranslationsToJson.ts translations.json translations.xlsx
```

### Excel File Format (Standard Translations)

The `translations.xlsx` file uses a **single-sheet, rotated format** where each language is a column:

| key          | en-US        | ko        | ar        | de-DE              |
| ------------ | ------------ | --------- | --------- | ------------------ |
| Add Language | Add Language | 언어 추가 | إضافة لغة | Sprache hinzufügen |
| Cancel       | Cancel       | 취소      | إلغاء     | Abbrechen          |

- The first column is always `key`
- All subsequent columns are language codes (e.g., `en-US`, `ko`, `ar`, `de-DE`)
- The fixed language order in the conversion script is: `en-US`, `de-DE`, `ko`, `ar` (additional languages are sorted alphabetically after these)

## Features

### 1. **Language Selection**

**Default Language**

- Defines the base language used as the fallback when translations are missing in other languages.

- Can be set from the dropdown at the top of the Languages page.

- Serves as the template for creating new custom languages (new languages are initialized with all keys from the default language's standard translations).

**Edit Language**

- Select which language dictionary to view and edit in the translation grid.

- Displays all translation key-value pairs, merging both standard and custom translations.

- Both standard and custom languages appear in this dropdown.

### 2. **Translation Grid**

- Displays a merged view of all translation keys and their corresponding values for the selected language.

- **Visual Indicators**:
  Rows with:
  - **Green dot**: Custom translation (editable, stored in CustomTranslations table). Custom values are displayed in **bold**.
  - **Yellow/Orange dot**: Standard translation (from StandardTranslations table). These rows indicate keys that only exist in standard translations and have no custom override yet.

![Visual Indicator](media/admin_guide_language_dot_indicator.png)

- **Key Column**: Contains the translation key (read-only, cannot be edited).

- **Value Column**: Contains the translated text (editable).

- **Editing Translations**:
  - Click the edit icon on a row to modify its value.
  - Edits to standard translation rows (**Yellow/Orange dot**) automatically create a custom override, promoting them to the CustomTranslations table for the edited language. The dot changes to green after saving.
  - All edits are persisted immediately upon saving the row.

- Features a filter row for quick searching of specific translations by key or value.

### 3. **Language Import / Export**

The **Language Import / Export** popup provides two actions: downloading all current translations and uploading updated or new translations. Click the **Upload languages as xlsx** button in the header to open it.

**Download current languages as xlsx**

1. Click the **Download current languages as xlsx** button inside the popup.

2. An Excel file is generated containing:
   - A `key` column with all translation keys
   - One column per language code (e.g., `en-US`, `de-DE`) with the current merged translation values (custom overrides take priority over standard translations)

**Upload languages as xlsx**

1. Click the **Upload languages as xlsx** button inside the popup to select an `.xlsx` or `.xls` file.

2. The import begins immediately upon file selection — no extra confirmation step is needed.

3. A **two-level progress bar** is displayed showing:
   - Which language is being imported and the language count (e.g., "Importing de-DE (2/4 languages)")
   - Key-level progress within the current language (e.g., "400 / 1200 keys")
   - A main progress bar for key progress and a thin bar for overall language progress

4. When complete, the popup displays:
   - A **success message** with the list of imported language codes, or
   - An **error message** if something went wrong
   - Any **invalid column names** that were skipped (shown in red)

**Important behavior**:

- **No languages are ever deleted** through this dialog. If a language exists in the system but is not present in the uploaded file, it remains untouched.
- If a language code is renamed in the Excel (e.g., `en-US` changed to `en-usa`), the renamed code is treated as a new language. The original `en-US` is not affected.
- Existing languages in the file are updated (custom translations are upserted). New language codes in the file are added.
- Language codes must be valid (2-3 letter base, optionally followed by a region code like `-US`, `-BR`). Invalid codes are shown to the user and skipped during import.

### 5. **Delete Custom Language**

Remove a custom language dictionary that is no longer needed:

1. Select the custom language to delete from the **Edit Language** dropdown.

2. The **Delete Custom Language** button appears (only visible when a custom language is selected, not for standard-only languages).

3. Click **Delete Custom Language**.

4. Confirm the deletion in the popup dialog.

5. The language is removed from the CustomTranslations table and the selection reverts to the default language.

**Note**: Deleting a custom language removes all custom translation overrides for that language. Standard translations are not affected.

## Managing Languages

### Update Existing Custom Translations (In detail)

1. Select the custom language to edit from the **Edit Language** dropdown.

2. Use the filter row to search for specific translations.

3. Click the edit icon button to edit the row.

4. Modify the value and click the **Save** button.

![Editing translations](media/admin_guide_languages_editing.gif)

### Set Default Language

1. Select the desired language from the **Default Language** dropdown.

2. The system updates the default language immediately.

3. The default language setting is stored in the database and persists across application restarts.

### Understanding Translation Priority

When the application displays a translation, it follows this priority order:

1. **Custom translation in user's current language** - User-specific overrides
2. **Standard translation in user's current language** - Base translations from the JSON/Excel file
3. **Custom translation in default language** - Fallback custom translations
4. **Standard translation in default language** - Fallback base translations
5. **`en-US` hardcoded fallback** - If the default language itself is not found
6. **`(Missing: <key>)` placeholder** - Displayed if no translation exists for the key in any source

This ensures users always see the most appropriate translation available.

## Excel File Format (Custom Language Import/Export)

Both the download and upload functions use the same **rotated format** where each language is a column:

| key           | en-US         | ko        | de-DE           |
| ------------- | ------------- | --------- | --------------- |
| Cancel        | Cancel        | 취소      | Abbrechen       |
| Save          | Save          | 저장      | Speichern       |
| Select a file | Select a file | 파일 선택 | Datei auswählen |

### Requirements

- The Excel file **must contain a header row** with a `key` column and one or more language code columns.
- Language codes in column headers must be valid (e.g., `en-US`, `ko`, `de-DE`, `ar`). Invalid codes are skipped with a warning.
- Each row represents a single translation entry.
- Keys must match the standard translation keys exactly (do not modify keys).
- Values should contain your custom translations.
- The system processes **all** language columns in the file at once.
- **No languages are deleted** during import. Only additions and updates are performed.

## Right-to-Left (RTL) Language Support

The application automatically supports Right-to-Left languages such as Arabic (`ar`) and Hebrew (`he`).

- When a language with RTL directionality is selected, the interface layout automatically adjusts to display content from right to left.

- Text alignment, menu positioning, and component layouts are automatically reversed to provide a natural reading experience.

- RTL support is determined by the language code. The following language prefixes trigger RTL mode: `ar`, `ar-SA`, `he`, `he-IL`, `fa`, `fa-IR`, `ur`, `ur-PK`.

- All UI components, including DevExtreme grids and form controls, adapt to the RTL layout automatically. The DevExtreme library is configured with `rtlEnabled: true` when an RTL language is active.

- RTL detection works by prefix matching: any language code starting with one of the RTL prefixes (e.g., `ar-EG`, `he-IL`) will trigger RTL mode.

![RTL](media/admin_guide_languages_rtl.gif)

## Technical Architecture

This section provides technical details about how the multi-language system is implemented for developers and advanced administrators.

### Database Schema

The translation system uses two database tables in the `common` schema:

**StandardTranslations**

| Column       | Type          | Description                                  |
| ------------ | ------------- | -------------------------------------------- |
| id           | Int (PK)      | Auto-incrementing primary key                |
| key          | NVarChar(500) | The translation key (e.g., "Add Language")   |
| translations | NVarChar(Max) | JSON object mapping language codes to values |

**CustomTranslations**

| Column       | Type          | Description                                  |
| ------------ | ------------- | -------------------------------------------- |
| id           | Int (PK)      | Auto-incrementing primary key                |
| key          | NVarChar(500) | The translation key (e.g., "Add Language")   |
| translations | NVarChar(Max) | JSON object mapping language codes to values |

Both tables store translations in a **JSON column** format. Each row represents one translation key, and the `translations` column contains a JSON object with language codes as keys:

```json
{ "en-US": "Cancel", "ko": "취소", "ar": "إلغاء", "de-DE": "Abbrechen" }
```

The default language setting is stored in the **Dictionary** table:

- `language`: `"SYSTEM_CONFIG"`
- `key`: `"DEFAULT_LANGUAGE"`
- `value`: The language code (e.g., `"en-US"`)

### Client-Side Architecture

**LanguageProvider Context** (`src/app/context/languageContext.tsx`):

- Wraps the entire application and provides the current dictionary, language, and direction to all components via React Context.
- On initialization, loads the default language and its merged dictionary.
- When a user's language preference is loaded from their account, fetches the corresponding merged dictionary.
- Exposes a `useLanguage()` hook that returns: `language`, `setLanguage`, `dictionary`, `direction` (`"ltr"` or `"rtl"`), `isRTL`, `refreshDictionary`, and `dictionaryReady`.
- Uses a JavaScript `Proxy` on the dictionary to return `(Missing: <key>)` for any key that is not found, making missing translations visible during development.

**RTLWrapper** (`src/app/layout.tsx`):

- A component in the root layout that monitors the current language via a `data-language` attribute on the HTML element.
- When the language changes, it sets the `dir` attribute on `<html>` to `"rtl"` or `"ltr"` and configures DevExtreme's `rtlEnabled` setting accordingly.

**DevExtreme Locale Integration** (`src/app/libs/useDevExtremeLocale.ts`):

- Loads the appropriate DevExtreme localization bundle (e.g., `ar`, `ko`, `de`) for built-in widget strings.
- Applies translation overrides from the application dictionary for common DevExtreme strings (e.g., "No data", "Search", "Cancel", filter operation labels).
- Sets the DevExtreme locale to match the user's language for proper date/number formatting.

### How Translations Are Used in Components

All translatable strings in the application are accessed through the `dictionary` object provided by the `useLanguage()` hook:

```tsx
const { dictionary } = useLanguage();

// Usage in JSX
<h1>{dictionary["Language Management"]}</h1>
<Button text={dictionary["Save"]} />
```

Translation keys are plain English strings (e.g., `"Add Language"`, `"Cancel"`, `"Failed to load translations"`). The dictionary is a key-value map where the key is the English string and the value is the translated string in the user's current language.

### Translation Validation Tests

The project includes automated tests (`__tests__/translations.test.ts`) that:

- Verify `translations.json` exists in the project root.
- Check that all translation keys used in the source code (`dictionary["..."]`) are present in `translations.json`.
- Warn about unused translation keys that exist in the file but are not referenced in the code.

Run translation tests with: `npx vitest run __tests__/translations.test.ts`

# General Settings

Navigate to **Settings** at the bottom of the navigation bar

### **Change the Toolkit Theme**

- Users can personalize the Toolkit's appearance by selecting a theme from a dropdown menu.

- Each theme modifies the application's visual style to match the selected design.

- Once a theme is chosen, the selection is saved.

- The selected theme is automatically applied across all future sessions for that user.

![Setting Theme](media/admin_guide_set_theme.gif)

### **Configure Language Options**

Users can select their preferred language from the **Language Settings** section.

- Use the dropdown menu to select from available languages (both standard and custom).

- The selected language is tied to the user account and is saved automatically.

- Users may select **"Default"** to always follow the system's default language. If the administrator changes the default language, users on "Default" will see the updated language automatically.

- The language preference is applied across all future sessions.

- If a translation is missing in the selected language, the system will fall back to:
  1. Standard translation in the selected language
  2. Custom translation in the default language
  3. Standard translation in the default language
  4. `en-US` as the ultimate fallback

- When a Right-to-Left language (e.g., Arabic, Hebrew) is selected, the entire interface layout automatically mirrors to RTL. See the [Right-to-Left (RTL) Language Support](#right-to-left-rtl-language-support) section for details.

**Note**: Only languages that have been configured (either as standard translations in the Excel file or as custom translations created by administrators) appear in this dropdown. For information on creating and managing languages, see the [Languages](#languages) section.

### **Signature Settings**

The Signature Settings section on the Settings page allows administrators to manage their own personal Interactions email signature.

- This signature is automatically included in the default message body when composing an Interactions email.

- Automatically appends a consistent sign-off to each email.

- Updates to the signature are saved to the user's profile and persist across sessions, eliminating the need to re-enter it each time.

![Setting Theme](media/user_guide_signature.png)

### **Company Settings**

The Company Settings section allows administrators to customize branding for the Toolkit application by setting a company name and uploading a company logo.

![Company Settings](media/admin_guide_company_info.png)

#### **Company Name**

- Enter your organization's name in the **Company Name** text field.

- The name will automatically save.

- This company name will appear on outgoing communications and throughout UI elements.

- Use the **Clear** button (X icon) to quickly remove the current company name if needed.

- A "Saving..." indicator appears briefly to confirm that your changes are being saved.

#### **Company Logo**

- Upload a custom logo to represent your organization throughout the Toolkit interface.

- The logo uploader supports all standard image formats (PNG, JPG, JPEG, GIF, SVG, etc.).

- You can either **drag and drop** an image file into the upload area or **click to browse** and select a file from your computer.

- Once selected, the logo is automatically uploaded and saved.

- A preview of your uploaded logo is displayed below the uploader for verification.

- The selected logo persists across all sessions and is visible to all users.

**Best Practices:**

- Use a **square or wide image** for optimal display across different UI components
- **Transparent PNG** format is recommended for the best visual results
- Ensure the logo has sufficient contrast against both light and dark backgrounds
- Keep file sizes reasonable (under 2MB) for faster loading
